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