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