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