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