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