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