Merge branch 'feature/optimize_rich_media_parser' 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.Keys
15 alias Pleroma.Notification
16 alias Pleroma.Object
17 alias Pleroma.Registration
18 alias Pleroma.Repo
19 alias Pleroma.RepoStreamer
20 alias Pleroma.User
21 alias Pleroma.Web
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI
25 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
26 alias Pleroma.Web.OAuth
27 alias Pleroma.Web.OStatus
28 alias Pleroma.Web.RelMe
29 alias Pleroma.Web.Websub
30 alias Pleroma.Workers.BackgroundWorker
31
32 require Logger
33
34 @type t :: %__MODULE__{}
35
36 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
37
38 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
39 @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])?)*$/
40
41 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
42 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
43
44 schema "users" do
45 field(:bio, :string)
46 field(:email, :string)
47 field(:name, :string)
48 field(:nickname, :string)
49 field(:password_hash, :string)
50 field(:password, :string, virtual: true)
51 field(:password_confirmation, :string, virtual: true)
52 field(:following, {:array, :string}, default: [])
53 field(:ap_id, :string)
54 field(:avatar, :map)
55 field(:local, :boolean, default: true)
56 field(:follower_address, :string)
57 field(:following_address, :string)
58 field(:search_rank, :float, virtual: true)
59 field(:search_type, :integer, virtual: true)
60 field(:tags, {:array, :string}, default: [])
61 field(:last_refreshed_at, :naive_datetime_usec)
62 field(:last_digest_emailed_at, :naive_datetime)
63 has_many(:notifications, Notification)
64 has_many(:registrations, Registration)
65 embeds_one(:info, User.Info)
66
67 timestamps()
68 end
69
70 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
71 do: !Pleroma.Config.get([:instance, :account_activation_required])
72
73 def auth_active?(%User{}), do: true
74
75 def visible_for?(user, for_user \\ nil)
76
77 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
78
79 def visible_for?(%User{} = user, for_user) do
80 auth_active?(user) || superuser?(for_user)
81 end
82
83 def visible_for?(_, _), do: false
84
85 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
86 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
87 def superuser?(_), do: false
88
89 def avatar_url(user, options \\ []) do
90 case user.avatar do
91 %{"url" => [%{"href" => href} | _]} -> href
92 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
93 end
94 end
95
96 def banner_url(user, options \\ []) do
97 case user.info.banner do
98 %{"url" => [%{"href" => href} | _]} -> href
99 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
100 end
101 end
102
103 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
104 def profile_url(%User{ap_id: ap_id}), do: ap_id
105 def profile_url(_), do: nil
106
107 def ap_id(%User{nickname: nickname}) do
108 "#{Web.base_url()}/users/#{nickname}"
109 end
110
111 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
112 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
113
114 @spec ap_following(User.t()) :: Sring.t()
115 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
116 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
117
118 def user_info(%User{} = user, args \\ %{}) do
119 following_count =
120 if args[:following_count],
121 do: args[:following_count],
122 else: user.info.following_count || following_count(user)
123
124 follower_count =
125 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
126
127 %{
128 note_count: user.info.note_count,
129 locked: user.info.locked,
130 confirmation_pending: user.info.confirmation_pending,
131 default_scope: user.info.default_scope
132 }
133 |> Map.put(:following_count, following_count)
134 |> Map.put(:follower_count, follower_count)
135 end
136
137 def follow_state(%User{} = user, %User{} = target) do
138 follow_activity = Utils.fetch_latest_follow(user, target)
139
140 if follow_activity,
141 do: follow_activity.data["state"],
142 # Ideally this would be nil, but then Cachex does not commit the value
143 else: false
144 end
145
146 def get_cached_follow_state(user, target) do
147 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
148 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
149 end
150
151 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
152 Cachex.put(
153 :user_cache,
154 "follow_state:#{user_ap_id}|#{target_ap_id}",
155 state
156 )
157 end
158
159 def set_info_cache(user, args) do
160 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
161 end
162
163 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
164 def restrict_deactivated(query) do
165 from(u in query,
166 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
167 )
168 end
169
170 def following_count(%User{following: []}), do: 0
171
172 def following_count(%User{} = user) do
173 user
174 |> get_friends_query()
175 |> Repo.aggregate(:count, :id)
176 end
177
178 defp truncate_if_exists(params, key, max_length) do
179 if Map.has_key?(params, key) and is_binary(params[key]) do
180 {value, _chopped} = String.split_at(params[key], max_length)
181 Map.put(params, key, value)
182 else
183 params
184 end
185 end
186
187 def remote_user_creation(params) do
188 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
189 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
190
191 params =
192 params
193 |> Map.put(:info, params[:info] || %{})
194 |> truncate_if_exists(:name, name_limit)
195 |> truncate_if_exists(:bio, bio_limit)
196
197 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
198
199 changes =
200 %User{}
201 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
202 |> validate_required([:name, :ap_id])
203 |> unique_constraint(:nickname)
204 |> validate_format(:nickname, @email_regex)
205 |> validate_length(:bio, max: bio_limit)
206 |> validate_length(:name, max: name_limit)
207 |> put_change(:local, false)
208 |> put_embed(:info, info_cng)
209
210 if changes.valid? do
211 case info_cng.changes[:source_data] do
212 %{"followers" => followers, "following" => following} ->
213 changes
214 |> put_change(:follower_address, followers)
215 |> put_change(:following_address, following)
216
217 _ ->
218 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
219
220 changes
221 |> put_change(:follower_address, followers)
222 end
223 else
224 changes
225 end
226 end
227
228 def update_changeset(struct, params \\ %{}) do
229 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
230 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
231
232 struct
233 |> cast(params, [:bio, :name, :avatar, :following])
234 |> unique_constraint(:nickname)
235 |> validate_format(:nickname, local_nickname_regex())
236 |> validate_length(:bio, max: bio_limit)
237 |> validate_length(:name, min: 1, max: name_limit)
238 end
239
240 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) 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 = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
245 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
246
247 struct
248 |> cast(params, [
249 :bio,
250 :name,
251 :follower_address,
252 :following_address,
253 :avatar,
254 :last_refreshed_at
255 ])
256 |> unique_constraint(:nickname)
257 |> validate_format(:nickname, local_nickname_regex())
258 |> validate_length(:bio, max: bio_limit)
259 |> validate_length(:name, max: name_limit)
260 |> put_embed(:info, info_cng)
261 end
262
263 def password_update_changeset(struct, params) do
264 struct
265 |> cast(params, [:password, :password_confirmation])
266 |> validate_required([:password, :password_confirmation])
267 |> validate_confirmation(:password)
268 |> put_password_hash
269 end
270
271 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
272 def reset_password(%User{id: user_id} = user, data) do
273 multi =
274 Multi.new()
275 |> Multi.update(:user, password_update_changeset(user, data))
276 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
277 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
278
279 case Repo.transaction(multi) do
280 {:ok, %{user: user} = _} -> set_cache(user)
281 {:error, _, changeset, _} -> {:error, changeset}
282 end
283 end
284
285 def register_changeset(struct, params \\ %{}, opts \\ []) do
286 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
287 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
288
289 need_confirmation? =
290 if is_nil(opts[:need_confirmation]) do
291 Pleroma.Config.get([:instance, :account_activation_required])
292 else
293 opts[:need_confirmation]
294 end
295
296 info_change =
297 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
298
299 changeset =
300 struct
301 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
302 |> validate_required([:name, :nickname, :password, :password_confirmation])
303 |> validate_confirmation(:password)
304 |> unique_constraint(:email)
305 |> unique_constraint(:nickname)
306 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
307 |> validate_format(:nickname, local_nickname_regex())
308 |> validate_format(:email, @email_regex)
309 |> validate_length(:bio, max: bio_limit)
310 |> validate_length(:name, min: 1, max: name_limit)
311 |> put_change(:info, info_change)
312
313 changeset =
314 if opts[:external] do
315 changeset
316 else
317 validate_required(changeset, [:email])
318 end
319
320 if changeset.valid? do
321 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
322 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
323
324 changeset
325 |> put_password_hash
326 |> put_change(:ap_id, ap_id)
327 |> unique_constraint(:ap_id)
328 |> put_change(:following, [followers])
329 |> put_change(:follower_address, followers)
330 else
331 changeset
332 end
333 end
334
335 defp autofollow_users(user) do
336 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
337
338 autofollowed_users =
339 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
340 |> Repo.all()
341
342 follow_all(user, autofollowed_users)
343 end
344
345 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
346 def register(%Ecto.Changeset{} = changeset) do
347 with {:ok, user} <- Repo.insert(changeset),
348 {:ok, user} <- post_register_action(user) do
349 {:ok, user}
350 end
351 end
352
353 def post_register_action(%User{} = user) do
354 with {:ok, user} <- autofollow_users(user),
355 {:ok, user} <- set_cache(user),
356 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
357 {:ok, _} <- try_send_confirmation_email(user) do
358 {:ok, user}
359 end
360 end
361
362 def try_send_confirmation_email(%User{} = user) do
363 if user.info.confirmation_pending &&
364 Pleroma.Config.get([:instance, :account_activation_required]) do
365 user
366 |> Pleroma.Emails.UserEmail.account_confirmation_email()
367 |> Pleroma.Emails.Mailer.deliver_async()
368
369 {:ok, :enqueued}
370 else
371 {:ok, :noop}
372 end
373 end
374
375 def needs_update?(%User{local: true}), do: false
376
377 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
378
379 def needs_update?(%User{local: false} = user) do
380 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
381 end
382
383 def needs_update?(_), do: true
384
385 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
386 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
387 {:ok, follower}
388 end
389
390 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
391 follow(follower, followed)
392 end
393
394 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
395 if not User.ap_enabled?(followed) do
396 follow(follower, followed)
397 else
398 {:ok, follower}
399 end
400 end
401
402 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
403 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
404 def follow_all(follower, followeds) do
405 followed_addresses =
406 followeds
407 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
408 |> Enum.map(fn %{follower_address: fa} -> fa end)
409
410 q =
411 from(u in User,
412 where: u.id == ^follower.id,
413 update: [
414 set: [
415 following:
416 fragment(
417 "array(select distinct unnest (array_cat(?, ?)))",
418 u.following,
419 ^followed_addresses
420 )
421 ]
422 ],
423 select: u
424 )
425
426 {1, [follower]} = Repo.update_all(q, [])
427
428 Enum.each(followeds, fn followed ->
429 update_follower_count(followed)
430 end)
431
432 set_cache(follower)
433 end
434
435 def follow(%User{} = follower, %User{info: info} = followed) do
436 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
437 ap_followers = followed.follower_address
438
439 cond do
440 info.deactivated ->
441 {:error, "Could not follow user: You are deactivated."}
442
443 deny_follow_blocked and blocks?(followed, follower) ->
444 {:error, "Could not follow user: #{followed.nickname} blocked you."}
445
446 true ->
447 if !followed.local && follower.local && !ap_enabled?(followed) do
448 Websub.subscribe(follower, followed)
449 end
450
451 q =
452 from(u in User,
453 where: u.id == ^follower.id,
454 update: [push: [following: ^ap_followers]],
455 select: u
456 )
457
458 {1, [follower]} = Repo.update_all(q, [])
459
460 follower = maybe_update_following_count(follower)
461
462 {:ok, _} = update_follower_count(followed)
463
464 set_cache(follower)
465 end
466 end
467
468 def unfollow(%User{} = follower, %User{} = followed) do
469 ap_followers = followed.follower_address
470
471 if following?(follower, followed) and follower.ap_id != followed.ap_id do
472 q =
473 from(u in User,
474 where: u.id == ^follower.id,
475 update: [pull: [following: ^ap_followers]],
476 select: u
477 )
478
479 {1, [follower]} = Repo.update_all(q, [])
480
481 follower = maybe_update_following_count(follower)
482
483 {:ok, followed} = update_follower_count(followed)
484
485 set_cache(follower)
486
487 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
488 else
489 {:error, "Not subscribed!"}
490 end
491 end
492
493 @spec following?(User.t(), User.t()) :: boolean
494 def following?(%User{} = follower, %User{} = followed) do
495 Enum.member?(follower.following, followed.follower_address)
496 end
497
498 def locked?(%User{} = user) do
499 user.info.locked || false
500 end
501
502 def get_by_id(id) do
503 Repo.get_by(User, id: id)
504 end
505
506 def get_by_ap_id(ap_id) do
507 Repo.get_by(User, ap_id: ap_id)
508 end
509
510 def get_all_by_ap_id(ap_ids) do
511 from(u in __MODULE__,
512 where: u.ap_id in ^ap_ids
513 )
514 |> Repo.all()
515 end
516
517 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
518 # of the ap_id and the domain and tries to get that user
519 def get_by_guessed_nickname(ap_id) do
520 domain = URI.parse(ap_id).host
521 name = List.last(String.split(ap_id, "/"))
522 nickname = "#{name}@#{domain}"
523
524 get_cached_by_nickname(nickname)
525 end
526
527 def set_cache({:ok, user}), do: set_cache(user)
528 def set_cache({:error, err}), do: {:error, err}
529
530 def set_cache(%User{} = user) do
531 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
532 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
533 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
534 {:ok, user}
535 end
536
537 def update_and_set_cache(changeset) do
538 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
539 set_cache(user)
540 else
541 e -> e
542 end
543 end
544
545 def invalidate_cache(user) do
546 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
547 Cachex.del(:user_cache, "nickname:#{user.nickname}")
548 Cachex.del(:user_cache, "user_info:#{user.id}")
549 end
550
551 def get_cached_by_ap_id(ap_id) do
552 key = "ap_id:#{ap_id}"
553 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
554 end
555
556 def get_cached_by_id(id) do
557 key = "id:#{id}"
558
559 ap_id =
560 Cachex.fetch!(:user_cache, key, fn _ ->
561 user = get_by_id(id)
562
563 if user do
564 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
565 {:commit, user.ap_id}
566 else
567 {:ignore, ""}
568 end
569 end)
570
571 get_cached_by_ap_id(ap_id)
572 end
573
574 def get_cached_by_nickname(nickname) do
575 key = "nickname:#{nickname}"
576
577 Cachex.fetch!(:user_cache, key, fn ->
578 user_result = get_or_fetch_by_nickname(nickname)
579
580 case user_result do
581 {:ok, user} -> {:commit, user}
582 {:error, _error} -> {:ignore, nil}
583 end
584 end)
585 end
586
587 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
588 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
589
590 cond do
591 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
592 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
593
594 restrict_to_local == false ->
595 get_cached_by_nickname(nickname_or_id)
596
597 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
598 get_cached_by_nickname(nickname_or_id)
599
600 true ->
601 nil
602 end
603 end
604
605 def get_by_nickname(nickname) do
606 Repo.get_by(User, nickname: nickname) ||
607 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
608 Repo.get_by(User, nickname: local_nickname(nickname))
609 end
610 end
611
612 def get_by_email(email), do: Repo.get_by(User, email: email)
613
614 def get_by_nickname_or_email(nickname_or_email) do
615 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
616 end
617
618 def get_cached_user_info(user) do
619 key = "user_info:#{user.id}"
620 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
621 end
622
623 def fetch_by_nickname(nickname) do
624 ap_try = ActivityPub.make_user_from_nickname(nickname)
625
626 case ap_try do
627 {:ok, user} -> {:ok, user}
628 _ -> OStatus.make_user(nickname)
629 end
630 end
631
632 def get_or_fetch_by_nickname(nickname) do
633 with %User{} = user <- get_by_nickname(nickname) do
634 {:ok, user}
635 else
636 _e ->
637 with [_nick, _domain] <- String.split(nickname, "@"),
638 {:ok, user} <- fetch_by_nickname(nickname) do
639 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
640 fetch_initial_posts(user)
641 end
642
643 {:ok, user}
644 else
645 _e -> {:error, "not found " <> nickname}
646 end
647 end
648 end
649
650 @doc "Fetch some posts when the user has just been federated with"
651 def fetch_initial_posts(user) do
652 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
653 end
654
655 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
656 def get_followers_query(%User{} = user, nil) do
657 User.Query.build(%{followers: user, deactivated: false})
658 end
659
660 def get_followers_query(user, page) do
661 from(u in get_followers_query(user, nil))
662 |> User.Query.paginate(page, 20)
663 end
664
665 @spec get_followers_query(User.t()) :: Ecto.Query.t()
666 def get_followers_query(user), do: get_followers_query(user, nil)
667
668 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
669 def get_followers(user, page \\ nil) do
670 q = get_followers_query(user, page)
671
672 {:ok, Repo.all(q)}
673 end
674
675 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
676 def get_external_followers(user, page \\ nil) do
677 q =
678 user
679 |> get_followers_query(page)
680 |> User.Query.build(%{external: true})
681
682 {:ok, Repo.all(q)}
683 end
684
685 def get_followers_ids(user, page \\ nil) do
686 q = get_followers_query(user, page)
687
688 Repo.all(from(u in q, select: u.id))
689 end
690
691 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
692 def get_friends_query(%User{} = user, nil) do
693 User.Query.build(%{friends: user, deactivated: false})
694 end
695
696 def get_friends_query(user, page) do
697 from(u in get_friends_query(user, nil))
698 |> User.Query.paginate(page, 20)
699 end
700
701 @spec get_friends_query(User.t()) :: Ecto.Query.t()
702 def get_friends_query(user), do: get_friends_query(user, nil)
703
704 def get_friends(user, page \\ nil) do
705 q = get_friends_query(user, page)
706
707 {:ok, Repo.all(q)}
708 end
709
710 def get_friends_ids(user, page \\ nil) do
711 q = get_friends_query(user, page)
712
713 Repo.all(from(u in q, select: u.id))
714 end
715
716 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
717 def get_follow_requests(%User{} = user) do
718 users =
719 Activity.follow_requests_for_actor(user)
720 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
721 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
722 |> group_by([a, u], u.id)
723 |> select([a, u], u)
724 |> Repo.all()
725
726 {:ok, users}
727 end
728
729 def increase_note_count(%User{} = user) do
730 User
731 |> where(id: ^user.id)
732 |> update([u],
733 set: [
734 info:
735 fragment(
736 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
737 u.info,
738 u.info
739 )
740 ]
741 )
742 |> select([u], u)
743 |> Repo.update_all([])
744 |> case do
745 {1, [user]} -> set_cache(user)
746 _ -> {:error, user}
747 end
748 end
749
750 def decrease_note_count(%User{} = user) do
751 User
752 |> where(id: ^user.id)
753 |> update([u],
754 set: [
755 info:
756 fragment(
757 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
758 u.info,
759 u.info
760 )
761 ]
762 )
763 |> select([u], u)
764 |> Repo.update_all([])
765 |> case do
766 {1, [user]} -> set_cache(user)
767 _ -> {:error, user}
768 end
769 end
770
771 def update_note_count(%User{} = user) do
772 note_count_query =
773 from(
774 a in Object,
775 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
776 select: count(a.id)
777 )
778
779 note_count = Repo.one(note_count_query)
780
781 info_cng = User.Info.set_note_count(user.info, note_count)
782
783 user
784 |> change()
785 |> put_embed(:info, info_cng)
786 |> update_and_set_cache()
787 end
788
789 @spec maybe_fetch_follow_information(User.t()) :: User.t()
790 def maybe_fetch_follow_information(user) do
791 with {:ok, user} <- fetch_follow_information(user) do
792 user
793 else
794 e ->
795 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
796
797 user
798 end
799 end
800
801 def fetch_follow_information(user) do
802 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
803 info_cng = User.Info.follow_information_update(user.info, info)
804
805 changeset =
806 user
807 |> change()
808 |> put_embed(:info, info_cng)
809
810 update_and_set_cache(changeset)
811 else
812 {:error, _} = e -> e
813 e -> {:error, e}
814 end
815 end
816
817 def update_follower_count(%User{} = user) do
818 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
819 follower_count_query =
820 User.Query.build(%{followers: user, deactivated: false})
821 |> select([u], %{count: count(u.id)})
822
823 User
824 |> where(id: ^user.id)
825 |> join(:inner, [u], s in subquery(follower_count_query))
826 |> update([u, s],
827 set: [
828 info:
829 fragment(
830 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
831 u.info,
832 s.count
833 )
834 ]
835 )
836 |> select([u], u)
837 |> Repo.update_all([])
838 |> case do
839 {1, [user]} -> set_cache(user)
840 _ -> {:error, user}
841 end
842 else
843 {:ok, maybe_fetch_follow_information(user)}
844 end
845 end
846
847 @spec maybe_update_following_count(User.t()) :: User.t()
848 def maybe_update_following_count(%User{local: false} = user) do
849 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
850 maybe_fetch_follow_information(user)
851 else
852 user
853 end
854 end
855
856 def maybe_update_following_count(user), do: user
857
858 def remove_duplicated_following(%User{following: following} = user) do
859 uniq_following = Enum.uniq(following)
860
861 if length(following) == length(uniq_following) do
862 {:ok, user}
863 else
864 user
865 |> update_changeset(%{following: uniq_following})
866 |> update_and_set_cache()
867 end
868 end
869
870 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
871 def get_users_from_set(ap_ids, local_only \\ true) do
872 criteria = %{ap_id: ap_ids, deactivated: false}
873 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
874
875 User.Query.build(criteria)
876 |> Repo.all()
877 end
878
879 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
880 def get_recipients_from_activity(%Activity{recipients: to}) do
881 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
882 |> Repo.all()
883 end
884
885 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
886 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
887 info = muter.info
888
889 info_cng =
890 User.Info.add_to_mutes(info, ap_id)
891 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
892
893 cng =
894 change(muter)
895 |> put_embed(:info, info_cng)
896
897 update_and_set_cache(cng)
898 end
899
900 def unmute(muter, %{ap_id: ap_id}) do
901 info = muter.info
902
903 info_cng =
904 User.Info.remove_from_mutes(info, ap_id)
905 |> User.Info.remove_from_muted_notifications(info, ap_id)
906
907 cng =
908 change(muter)
909 |> put_embed(:info, info_cng)
910
911 update_and_set_cache(cng)
912 end
913
914 def subscribe(subscriber, %{ap_id: ap_id}) do
915 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
916
917 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
918 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
919
920 if blocked do
921 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
922 else
923 info_cng =
924 subscribed.info
925 |> User.Info.add_to_subscribers(subscriber.ap_id)
926
927 change(subscribed)
928 |> put_embed(:info, info_cng)
929 |> update_and_set_cache()
930 end
931 end
932 end
933
934 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
935 with %User{} = user <- get_cached_by_ap_id(ap_id) do
936 info_cng =
937 user.info
938 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
939
940 change(user)
941 |> put_embed(:info, info_cng)
942 |> update_and_set_cache()
943 end
944 end
945
946 def block(blocker, %User{ap_id: ap_id} = blocked) do
947 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
948 blocker =
949 if following?(blocker, blocked) do
950 {:ok, blocker, _} = unfollow(blocker, blocked)
951 blocker
952 else
953 blocker
954 end
955
956 # clear any requested follows as well
957 blocked =
958 case CommonAPI.reject_follow_request(blocked, blocker) do
959 {:ok, %User{} = updated_blocked} -> updated_blocked
960 nil -> blocked
961 end
962
963 blocker =
964 if subscribed_to?(blocked, blocker) do
965 {:ok, blocker} = unsubscribe(blocked, blocker)
966 blocker
967 else
968 blocker
969 end
970
971 if following?(blocked, blocker) do
972 unfollow(blocked, blocker)
973 end
974
975 {:ok, blocker} = update_follower_count(blocker)
976
977 info_cng =
978 blocker.info
979 |> User.Info.add_to_block(ap_id)
980
981 cng =
982 change(blocker)
983 |> put_embed(:info, info_cng)
984
985 update_and_set_cache(cng)
986 end
987
988 # helper to handle the block given only an actor's AP id
989 def block(blocker, %{ap_id: ap_id}) do
990 block(blocker, get_cached_by_ap_id(ap_id))
991 end
992
993 def unblock(blocker, %{ap_id: ap_id}) do
994 info_cng =
995 blocker.info
996 |> User.Info.remove_from_block(ap_id)
997
998 cng =
999 change(blocker)
1000 |> put_embed(:info, info_cng)
1001
1002 update_and_set_cache(cng)
1003 end
1004
1005 def mutes?(nil, _), do: false
1006 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1007
1008 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1009 def muted_notifications?(nil, _), do: false
1010
1011 def muted_notifications?(user, %{ap_id: ap_id}),
1012 do: Enum.member?(user.info.muted_notifications, ap_id)
1013
1014 def blocks?(%User{} = user, %User{} = target) do
1015 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1016 end
1017
1018 def blocks?(nil, _), do: false
1019
1020 def blocks_ap_id?(%User{} = user, %User{} = target) do
1021 Enum.member?(user.info.blocks, target.ap_id)
1022 end
1023
1024 def blocks_ap_id?(_, _), do: false
1025
1026 def blocks_domain?(%User{} = user, %User{} = target) do
1027 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1028 %{host: host} = URI.parse(target.ap_id)
1029 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1030 end
1031
1032 def blocks_domain?(_, _), do: false
1033
1034 def subscribed_to?(user, %{ap_id: ap_id}) do
1035 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1036 Enum.member?(target.info.subscribers, user.ap_id)
1037 end
1038 end
1039
1040 @spec muted_users(User.t()) :: [User.t()]
1041 def muted_users(user) do
1042 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1043 |> Repo.all()
1044 end
1045
1046 @spec blocked_users(User.t()) :: [User.t()]
1047 def blocked_users(user) do
1048 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1049 |> Repo.all()
1050 end
1051
1052 @spec subscribers(User.t()) :: [User.t()]
1053 def subscribers(user) do
1054 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1055 |> Repo.all()
1056 end
1057
1058 def block_domain(user, domain) do
1059 info_cng =
1060 user.info
1061 |> User.Info.add_to_domain_block(domain)
1062
1063 cng =
1064 change(user)
1065 |> put_embed(:info, info_cng)
1066
1067 update_and_set_cache(cng)
1068 end
1069
1070 def unblock_domain(user, domain) do
1071 info_cng =
1072 user.info
1073 |> User.Info.remove_from_domain_block(domain)
1074
1075 cng =
1076 change(user)
1077 |> put_embed(:info, info_cng)
1078
1079 update_and_set_cache(cng)
1080 end
1081
1082 def deactivate_async(user, status \\ true) do
1083 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1084 end
1085
1086 def deactivate(%User{} = user, status \\ true) do
1087 info_cng = User.Info.set_activation_status(user.info, status)
1088
1089 with {:ok, friends} <- User.get_friends(user),
1090 {:ok, followers} <- User.get_followers(user),
1091 {:ok, user} <-
1092 user
1093 |> change()
1094 |> put_embed(:info, info_cng)
1095 |> update_and_set_cache() do
1096 Enum.each(followers, &invalidate_cache(&1))
1097 Enum.each(friends, &update_follower_count(&1))
1098
1099 {:ok, user}
1100 end
1101 end
1102
1103 def update_notification_settings(%User{} = user, settings \\ %{}) do
1104 info_changeset = User.Info.update_notification_settings(user.info, settings)
1105
1106 change(user)
1107 |> put_embed(:info, info_changeset)
1108 |> update_and_set_cache()
1109 end
1110
1111 def delete(%User{} = user) do
1112 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1113 end
1114
1115 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1116 def perform(:delete, %User{} = user) do
1117 {:ok, _user} = ActivityPub.delete(user)
1118
1119 # Remove all relationships
1120 {:ok, followers} = User.get_followers(user)
1121
1122 Enum.each(followers, fn follower ->
1123 ActivityPub.unfollow(follower, user)
1124 User.unfollow(follower, user)
1125 end)
1126
1127 {:ok, friends} = User.get_friends(user)
1128
1129 Enum.each(friends, fn followed ->
1130 ActivityPub.unfollow(user, followed)
1131 User.unfollow(user, followed)
1132 end)
1133
1134 delete_user_activities(user)
1135 invalidate_cache(user)
1136 Repo.delete(user)
1137 end
1138
1139 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1140 def perform(:fetch_initial_posts, %User{} = user) do
1141 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1142
1143 Enum.each(
1144 # Insert all the posts in reverse order, so they're in the right order on the timeline
1145 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1146 &Pleroma.Web.Federator.incoming_ap_doc/1
1147 )
1148
1149 {:ok, user}
1150 end
1151
1152 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1153
1154 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1155 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1156 when is_list(blocked_identifiers) do
1157 Enum.map(
1158 blocked_identifiers,
1159 fn blocked_identifier ->
1160 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1161 {:ok, blocker} <- block(blocker, blocked),
1162 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1163 blocked
1164 else
1165 err ->
1166 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1167 err
1168 end
1169 end
1170 )
1171 end
1172
1173 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1174 def perform(:follow_import, %User{} = follower, followed_identifiers)
1175 when is_list(followed_identifiers) do
1176 Enum.map(
1177 followed_identifiers,
1178 fn followed_identifier ->
1179 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1180 {:ok, follower} <- maybe_direct_follow(follower, followed),
1181 {:ok, _} <- ActivityPub.follow(follower, followed) do
1182 followed
1183 else
1184 err ->
1185 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1186 err
1187 end
1188 end
1189 )
1190 end
1191
1192 @spec external_users_query() :: Ecto.Query.t()
1193 def external_users_query do
1194 User.Query.build(%{
1195 external: true,
1196 active: true,
1197 order_by: :id
1198 })
1199 end
1200
1201 @spec external_users(keyword()) :: [User.t()]
1202 def external_users(opts \\ []) do
1203 query =
1204 external_users_query()
1205 |> select([u], struct(u, [:id, :ap_id, :info]))
1206
1207 query =
1208 if opts[:max_id],
1209 do: where(query, [u], u.id > ^opts[:max_id]),
1210 else: query
1211
1212 query =
1213 if opts[:limit],
1214 do: limit(query, ^opts[:limit]),
1215 else: query
1216
1217 Repo.all(query)
1218 end
1219
1220 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1221 BackgroundWorker.enqueue("blocks_import", %{
1222 "blocker_id" => blocker.id,
1223 "blocked_identifiers" => blocked_identifiers
1224 })
1225 end
1226
1227 def follow_import(%User{} = follower, followed_identifiers)
1228 when is_list(followed_identifiers) do
1229 BackgroundWorker.enqueue("follow_import", %{
1230 "follower_id" => follower.id,
1231 "followed_identifiers" => followed_identifiers
1232 })
1233 end
1234
1235 def delete_user_activities(%User{ap_id: ap_id} = user) do
1236 ap_id
1237 |> Activity.Queries.by_actor()
1238 |> RepoStreamer.chunk_stream(50)
1239 |> Stream.each(fn activities ->
1240 Enum.each(activities, &delete_activity(&1))
1241 end)
1242 |> Stream.run()
1243
1244 {:ok, user}
1245 end
1246
1247 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1248 activity
1249 |> Object.normalize()
1250 |> ActivityPub.delete()
1251 end
1252
1253 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1254 user = get_cached_by_ap_id(activity.actor)
1255 object = Object.normalize(activity)
1256
1257 ActivityPub.unlike(user, object)
1258 end
1259
1260 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1261 user = get_cached_by_ap_id(activity.actor)
1262 object = Object.normalize(activity)
1263
1264 ActivityPub.unannounce(user, object)
1265 end
1266
1267 defp delete_activity(_activity), do: "Doing nothing"
1268
1269 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1270 Pleroma.HTML.Scrubber.TwitterText
1271 end
1272
1273 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1274
1275 def fetch_by_ap_id(ap_id) do
1276 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1277
1278 case ap_try do
1279 {:ok, user} ->
1280 {:ok, user}
1281
1282 _ ->
1283 case OStatus.make_user(ap_id) do
1284 {:ok, user} -> {:ok, user}
1285 _ -> {:error, "Could not fetch by AP id"}
1286 end
1287 end
1288 end
1289
1290 def get_or_fetch_by_ap_id(ap_id) do
1291 user = get_cached_by_ap_id(ap_id)
1292
1293 if !is_nil(user) and !User.needs_update?(user) do
1294 {:ok, user}
1295 else
1296 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1297 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1298
1299 resp = fetch_by_ap_id(ap_id)
1300
1301 if should_fetch_initial do
1302 with {:ok, %User{} = user} <- resp do
1303 fetch_initial_posts(user)
1304 end
1305 end
1306
1307 resp
1308 end
1309 end
1310
1311 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1312 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1313 if user = get_cached_by_ap_id(uri) do
1314 user
1315 else
1316 changes =
1317 %User{info: %User.Info{}}
1318 |> cast(%{}, [:ap_id, :nickname, :local])
1319 |> put_change(:ap_id, uri)
1320 |> put_change(:nickname, nickname)
1321 |> put_change(:local, true)
1322 |> put_change(:follower_address, uri <> "/followers")
1323
1324 {:ok, user} = Repo.insert(changes)
1325 user
1326 end
1327 end
1328
1329 # AP style
1330 def public_key_from_info(%{
1331 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1332 }) do
1333 key =
1334 public_key_pem
1335 |> :public_key.pem_decode()
1336 |> hd()
1337 |> :public_key.pem_entry_decode()
1338
1339 {:ok, key}
1340 end
1341
1342 # OStatus Magic Key
1343 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1344 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1345 end
1346
1347 def public_key_from_info(_), do: {:error, "not found key"}
1348
1349 def get_public_key_for_ap_id(ap_id) do
1350 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1351 {:ok, public_key} <- public_key_from_info(user.info) do
1352 {:ok, public_key}
1353 else
1354 _ -> :error
1355 end
1356 end
1357
1358 defp blank?(""), do: nil
1359 defp blank?(n), do: n
1360
1361 def insert_or_update_user(data) do
1362 data
1363 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1364 |> remote_user_creation()
1365 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1366 |> set_cache()
1367 end
1368
1369 def ap_enabled?(%User{local: true}), do: true
1370 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1371 def ap_enabled?(_), do: false
1372
1373 @doc "Gets or fetch a user by uri or nickname."
1374 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1375 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1376 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1377
1378 # wait a period of time and return newest version of the User structs
1379 # this is because we have synchronous follow APIs and need to simulate them
1380 # with an async handshake
1381 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1382 with %User{} = a <- User.get_cached_by_id(a.id),
1383 %User{} = b <- User.get_cached_by_id(b.id) do
1384 {:ok, a, b}
1385 else
1386 _e ->
1387 :error
1388 end
1389 end
1390
1391 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1392 with :ok <- :timer.sleep(timeout),
1393 %User{} = a <- User.get_cached_by_id(a.id),
1394 %User{} = b <- User.get_cached_by_id(b.id) do
1395 {:ok, a, b}
1396 else
1397 _e ->
1398 :error
1399 end
1400 end
1401
1402 def parse_bio(bio) when is_binary(bio) and bio != "" do
1403 bio
1404 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1405 |> elem(0)
1406 end
1407
1408 def parse_bio(_), do: ""
1409
1410 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1411 # TODO: get profile URLs other than user.ap_id
1412 profile_urls = [user.ap_id]
1413
1414 bio
1415 |> CommonUtils.format_input("text/plain",
1416 mentions_format: :full,
1417 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1418 )
1419 |> elem(0)
1420 end
1421
1422 def parse_bio(_, _), do: ""
1423
1424 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1425 Repo.transaction(fn ->
1426 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1427 end)
1428 end
1429
1430 def tag(nickname, tags) when is_binary(nickname),
1431 do: tag(get_by_nickname(nickname), tags)
1432
1433 def tag(%User{} = user, tags),
1434 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1435
1436 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1437 Repo.transaction(fn ->
1438 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1439 end)
1440 end
1441
1442 def untag(nickname, tags) when is_binary(nickname),
1443 do: untag(get_by_nickname(nickname), tags)
1444
1445 def untag(%User{} = user, tags),
1446 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1447
1448 defp update_tags(%User{} = user, new_tags) do
1449 {:ok, updated_user} =
1450 user
1451 |> change(%{tags: new_tags})
1452 |> update_and_set_cache()
1453
1454 updated_user
1455 end
1456
1457 defp normalize_tags(tags) do
1458 [tags]
1459 |> List.flatten()
1460 |> Enum.map(&String.downcase(&1))
1461 end
1462
1463 defp local_nickname_regex do
1464 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1465 @extended_local_nickname_regex
1466 else
1467 @strict_local_nickname_regex
1468 end
1469 end
1470
1471 def local_nickname(nickname_or_mention) do
1472 nickname_or_mention
1473 |> full_nickname()
1474 |> String.split("@")
1475 |> hd()
1476 end
1477
1478 def full_nickname(nickname_or_mention),
1479 do: String.trim_leading(nickname_or_mention, "@")
1480
1481 def error_user(ap_id) do
1482 %User{
1483 name: ap_id,
1484 ap_id: ap_id,
1485 info: %User.Info{},
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.info.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: fragment("not (?->'deactivated' @> 'true')", u.info),
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{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1545 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1546
1547 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1548 Pleroma.User{info: %{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 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1554
1555 change(user)
1556 |> put_embed(:info, info)
1557 |> update_and_set_cache()
1558 end
1559
1560 @doc """
1561 Set `last_digest_emailed_at` value for the user to current time
1562 """
1563 @spec touch_last_digest_emailed_at(t()) :: t()
1564 def touch_last_digest_emailed_at(user) do
1565 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1566
1567 {:ok, updated_user} =
1568 user
1569 |> change(%{last_digest_emailed_at: now})
1570 |> update_and_set_cache()
1571
1572 updated_user
1573 end
1574
1575 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1576 def toggle_confirmation(%User{} = user) do
1577 need_confirmation? = !user.info.confirmation_pending
1578
1579 info_changeset =
1580 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1581
1582 user
1583 |> change()
1584 |> put_embed(:info, info_changeset)
1585 |> update_and_set_cache()
1586 end
1587
1588 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1589 mascot
1590 end
1591
1592 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1593 # use instance-default
1594 config = Pleroma.Config.get([:assets, :mascots])
1595 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1596 mascot = Keyword.get(config, default_mascot)
1597
1598 %{
1599 "id" => "default-mascot",
1600 "url" => mascot[:url],
1601 "preview_url" => mascot[:url],
1602 "pleroma" => %{
1603 "mime_type" => mascot[:mime_type]
1604 }
1605 }
1606 end
1607
1608 def ensure_keys_present(%User{info: info} = user) do
1609 if info.keys do
1610 {:ok, user}
1611 else
1612 {:ok, pem} = Keys.generate_rsa_pem()
1613
1614 user
1615 |> Ecto.Changeset.change()
1616 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1617 |> update_and_set_cache()
1618 end
1619 end
1620
1621 def get_ap_ids_by_nicknames(nicknames) do
1622 from(u in User,
1623 where: u.nickname in ^nicknames,
1624 select: u.ap_id
1625 )
1626 |> Repo.all()
1627 end
1628
1629 defdelegate search(query, opts \\ []), to: User.Search
1630
1631 defp put_password_hash(
1632 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1633 ) do
1634 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1635 end
1636
1637 defp put_password_hash(changeset), do: changeset
1638
1639 def is_internal_user?(%User{nickname: nil}), do: true
1640 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1641 def is_internal_user?(_), do: false
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 end