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