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