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