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