add tests
[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_deactivated(query) do
121 from(u in query,
122 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> '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_deactivated()
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_deactivated()
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_deactivated()
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_deactivated()
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_deactivated()
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_deactivated()
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_async(user, status \\ true) do
1154 PleromaJobQueue.enqueue(:user, __MODULE__, [:deactivate_async, user, status])
1155 end
1156
1157 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1158
1159 def deactivate(%User{} = user, status \\ true) do
1160 info_cng = User.Info.set_activation_status(user.info, status)
1161
1162 with {:ok, friends} <- User.get_friends(user),
1163 {:ok, followers} <- User.get_followers(user),
1164 {:ok, user} <-
1165 user
1166 |> change()
1167 |> put_embed(:info, info_cng)
1168 |> update_and_set_cache() do
1169 Enum.each(followers, &invalidate_cache(&1))
1170 Enum.each(friends, &update_follower_count(&1))
1171
1172 {:ok, user}
1173 end
1174 end
1175
1176 def update_notification_settings(%User{} = user, settings \\ %{}) do
1177 info_changeset = User.Info.update_notification_settings(user.info, settings)
1178
1179 change(user)
1180 |> put_embed(:info, info_changeset)
1181 |> update_and_set_cache()
1182 end
1183
1184 def delete(%User{} = user) do
1185 {:ok, user} = User.deactivate(user)
1186
1187 # Remove all relationships
1188 {:ok, followers} = User.get_followers(user)
1189
1190 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1191
1192 {:ok, friends} = User.get_friends(user)
1193
1194 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1195
1196 delete_user_activities(user)
1197 end
1198
1199 def delete_user_activities(%User{ap_id: ap_id} = user) do
1200 Activity
1201 |> where(actor: ^ap_id)
1202 |> Activity.with_preloaded_object()
1203 |> Repo.all()
1204 |> Enum.each(fn
1205 %{data: %{"type" => "Create"}} = activity ->
1206 activity |> Object.normalize() |> ActivityPub.delete()
1207
1208 # TODO: Do something with likes, follows, repeats.
1209 _ ->
1210 "Doing nothing"
1211 end)
1212
1213 {:ok, user}
1214 end
1215
1216 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1217 Pleroma.HTML.Scrubber.TwitterText
1218 end
1219
1220 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1221
1222 def html_filter_policy(_), do: @default_scrubbers
1223
1224 def fetch_by_ap_id(ap_id) do
1225 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1226
1227 case ap_try do
1228 {:ok, user} ->
1229 user
1230
1231 _ ->
1232 case OStatus.make_user(ap_id) do
1233 {:ok, user} -> user
1234 _ -> {:error, "Could not fetch by AP id"}
1235 end
1236 end
1237 end
1238
1239 def get_or_fetch_by_ap_id(ap_id) do
1240 user = get_by_ap_id(ap_id)
1241
1242 if !is_nil(user) and !User.needs_update?(user) do
1243 user
1244 else
1245 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1246 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1247
1248 user = fetch_by_ap_id(ap_id)
1249
1250 if should_fetch_initial do
1251 with %User{} = user do
1252 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1253 end
1254 end
1255
1256 user
1257 end
1258 end
1259
1260 def get_or_create_instance_user do
1261 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1262
1263 if user = get_by_ap_id(relay_uri) do
1264 user
1265 else
1266 changes =
1267 %User{info: %User.Info{}}
1268 |> cast(%{}, [:ap_id, :nickname, :local])
1269 |> put_change(:ap_id, relay_uri)
1270 |> put_change(:nickname, nil)
1271 |> put_change(:local, true)
1272 |> put_change(:follower_address, relay_uri <> "/followers")
1273
1274 {:ok, user} = Repo.insert(changes)
1275 user
1276 end
1277 end
1278
1279 # AP style
1280 def public_key_from_info(%{
1281 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1282 }) do
1283 key =
1284 public_key_pem
1285 |> :public_key.pem_decode()
1286 |> hd()
1287 |> :public_key.pem_entry_decode()
1288
1289 {:ok, key}
1290 end
1291
1292 # OStatus Magic Key
1293 def public_key_from_info(%{magic_key: magic_key}) do
1294 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1295 end
1296
1297 def get_public_key_for_ap_id(ap_id) do
1298 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1299 {:ok, public_key} <- public_key_from_info(user.info) do
1300 {:ok, public_key}
1301 else
1302 _ -> :error
1303 end
1304 end
1305
1306 defp blank?(""), do: nil
1307 defp blank?(n), do: n
1308
1309 def insert_or_update_user(data) do
1310 data =
1311 data
1312 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1313
1314 cs = User.remote_user_creation(data)
1315
1316 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1317 end
1318
1319 def ap_enabled?(%User{local: true}), do: true
1320 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1321 def ap_enabled?(_), do: false
1322
1323 @doc "Gets or fetch a user by uri or nickname."
1324 @spec get_or_fetch(String.t()) :: User.t()
1325 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1326 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1327
1328 # wait a period of time and return newest version of the User structs
1329 # this is because we have synchronous follow APIs and need to simulate them
1330 # with an async handshake
1331 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1332 with %User{} = a <- User.get_by_id(a.id),
1333 %User{} = b <- User.get_by_id(b.id) do
1334 {:ok, a, b}
1335 else
1336 _e ->
1337 :error
1338 end
1339 end
1340
1341 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1342 with :ok <- :timer.sleep(timeout),
1343 %User{} = a <- User.get_by_id(a.id),
1344 %User{} = b <- User.get_by_id(b.id) do
1345 {:ok, a, b}
1346 else
1347 _e ->
1348 :error
1349 end
1350 end
1351
1352 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1353 def parse_bio(nil, _user), do: ""
1354 def parse_bio(bio, _user) when bio == "", do: bio
1355
1356 def parse_bio(bio, user) do
1357 emoji =
1358 (user.info.source_data["tag"] || [])
1359 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1360 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1361 {String.trim(name, ":"), url}
1362 end)
1363
1364 # TODO: get profile URLs other than user.ap_id
1365 profile_urls = [user.ap_id]
1366
1367 bio
1368 |> CommonUtils.format_input("text/plain",
1369 mentions_format: :full,
1370 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1371 )
1372 |> elem(0)
1373 |> Formatter.emojify(emoji)
1374 end
1375
1376 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1377 Repo.transaction(fn ->
1378 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1379 end)
1380 end
1381
1382 def tag(nickname, tags) when is_binary(nickname),
1383 do: tag(User.get_by_nickname(nickname), tags)
1384
1385 def tag(%User{} = user, tags),
1386 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1387
1388 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1389 Repo.transaction(fn ->
1390 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1391 end)
1392 end
1393
1394 def untag(nickname, tags) when is_binary(nickname),
1395 do: untag(User.get_by_nickname(nickname), tags)
1396
1397 def untag(%User{} = user, tags),
1398 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1399
1400 defp update_tags(%User{} = user, new_tags) do
1401 {:ok, updated_user} =
1402 user
1403 |> change(%{tags: new_tags})
1404 |> update_and_set_cache()
1405
1406 updated_user
1407 end
1408
1409 def bookmark(%User{} = user, status_id) do
1410 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1411 update_bookmarks(user, bookmarks)
1412 end
1413
1414 def unbookmark(%User{} = user, status_id) do
1415 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1416 update_bookmarks(user, bookmarks)
1417 end
1418
1419 def update_bookmarks(%User{} = user, bookmarks) do
1420 user
1421 |> change(%{bookmarks: bookmarks})
1422 |> update_and_set_cache
1423 end
1424
1425 defp normalize_tags(tags) do
1426 [tags]
1427 |> List.flatten()
1428 |> Enum.map(&String.downcase(&1))
1429 end
1430
1431 defp local_nickname_regex do
1432 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1433 @extended_local_nickname_regex
1434 else
1435 @strict_local_nickname_regex
1436 end
1437 end
1438
1439 def local_nickname(nickname_or_mention) do
1440 nickname_or_mention
1441 |> full_nickname()
1442 |> String.split("@")
1443 |> hd()
1444 end
1445
1446 def full_nickname(nickname_or_mention),
1447 do: String.trim_leading(nickname_or_mention, "@")
1448
1449 def error_user(ap_id) do
1450 %User{
1451 name: ap_id,
1452 ap_id: ap_id,
1453 info: %User.Info{},
1454 nickname: "erroruser@example.com",
1455 inserted_at: NaiveDateTime.utc_now()
1456 }
1457 end
1458
1459 def all_superusers do
1460 from(
1461 u in User,
1462 where: u.local == true,
1463 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1464 )
1465 |> Repo.all()
1466 end
1467
1468 defp paginate(query, page, page_size) do
1469 from(u in query,
1470 limit: ^page_size,
1471 offset: ^((page - 1) * page_size)
1472 )
1473 end
1474
1475 def showing_reblogs?(%User{} = user, %User{} = target) do
1476 target.ap_id not in user.info.muted_reblogs
1477 end
1478 end