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