78543b4265b24430b12845b9b16eb5ca3e6bd99c
[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(u in get_followers_query(user, nil))
551 |> paginate(page, 20)
552 end
553
554 def get_followers_query(user), do: get_followers_query(user, nil)
555
556 def get_followers(user, page \\ nil) do
557 q = get_followers_query(user, page)
558
559 {:ok, Repo.all(q)}
560 end
561
562 def get_followers_ids(user, page \\ nil) do
563 q = get_followers_query(user, page)
564
565 Repo.all(from(u in q, select: u.id))
566 end
567
568 def get_friends_query(%User{id: id, following: following}, nil) do
569 from(
570 u in User,
571 where: u.follower_address in ^following,
572 where: u.id != ^id
573 )
574 end
575
576 def get_friends_query(user, page) do
577 from(u in get_friends_query(user, nil))
578 |> paginate(page, 20)
579 end
580
581 def get_friends_query(user), do: get_friends_query(user, nil)
582
583 def get_friends(user, page \\ nil) do
584 q = get_friends_query(user, page)
585
586 {:ok, Repo.all(q)}
587 end
588
589 def get_friends_ids(user, page \\ nil) do
590 q = get_friends_query(user, page)
591
592 Repo.all(from(u in q, select: u.id))
593 end
594
595 def get_follow_requests_query(%User{} = user) do
596 from(
597 a in Activity,
598 where:
599 fragment(
600 "? ->> 'type' = 'Follow'",
601 a.data
602 ),
603 where:
604 fragment(
605 "? ->> 'state' = 'pending'",
606 a.data
607 ),
608 where:
609 fragment(
610 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
611 a.data,
612 a.data,
613 ^user.ap_id
614 )
615 )
616 end
617
618 def get_follow_requests(%User{} = user) do
619 users =
620 user
621 |> User.get_follow_requests_query()
622 |> join(:inner, [a], u in User, a.actor == u.ap_id)
623 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
624 |> group_by([a, u], u.id)
625 |> select([a, u], u)
626 |> Repo.all()
627
628 {:ok, users}
629 end
630
631 def increase_note_count(%User{} = user) do
632 User
633 |> where(id: ^user.id)
634 |> update([u],
635 set: [
636 info:
637 fragment(
638 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
639 u.info,
640 u.info
641 )
642 ]
643 )
644 |> Repo.update_all([], returning: true)
645 |> case do
646 {1, [user]} -> set_cache(user)
647 _ -> {:error, user}
648 end
649 end
650
651 def decrease_note_count(%User{} = user) do
652 User
653 |> where(id: ^user.id)
654 |> update([u],
655 set: [
656 info:
657 fragment(
658 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
659 u.info,
660 u.info
661 )
662 ]
663 )
664 |> Repo.update_all([], returning: true)
665 |> case do
666 {1, [user]} -> set_cache(user)
667 _ -> {:error, user}
668 end
669 end
670
671 def update_note_count(%User{} = user) do
672 note_count_query =
673 from(
674 a in Object,
675 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
676 select: count(a.id)
677 )
678
679 note_count = Repo.one(note_count_query)
680
681 info_cng = User.Info.set_note_count(user.info, note_count)
682
683 cng =
684 change(user)
685 |> put_embed(:info, info_cng)
686
687 update_and_set_cache(cng)
688 end
689
690 def update_follower_count(%User{} = user) do
691 follower_count_query =
692 User
693 |> where([u], ^user.follower_address in u.following)
694 |> where([u], u.id != ^user.id)
695 |> select([u], %{count: count(u.id)})
696
697 User
698 |> where(id: ^user.id)
699 |> join(:inner, [u], s in subquery(follower_count_query))
700 |> update([u, s],
701 set: [
702 info:
703 fragment(
704 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
705 u.info,
706 s.count
707 )
708 ]
709 )
710 |> Repo.update_all([], returning: true)
711 |> case do
712 {1, [user]} -> set_cache(user)
713 _ -> {:error, user}
714 end
715 end
716
717 def get_users_from_set_query(ap_ids, false) do
718 from(
719 u in User,
720 where: u.ap_id in ^ap_ids
721 )
722 end
723
724 def get_users_from_set_query(ap_ids, true) do
725 query = get_users_from_set_query(ap_ids, false)
726
727 from(
728 u in query,
729 where: u.local == true
730 )
731 end
732
733 def get_users_from_set(ap_ids, local_only \\ true) do
734 get_users_from_set_query(ap_ids, local_only)
735 |> Repo.all()
736 end
737
738 def get_recipients_from_activity(%Activity{recipients: to}) do
739 query =
740 from(
741 u in User,
742 where: u.ap_id in ^to,
743 or_where: fragment("? && ?", u.following, ^to)
744 )
745
746 query = from(u in query, where: u.local == true)
747
748 Repo.all(query)
749 end
750
751 @spec search_for_admin(binary(), %{
752 admin: Pleroma.User.t(),
753 local: boolean(),
754 page: number(),
755 page_size: number()
756 }) :: {:ok, [Pleroma.User.t()], number()}
757 def search_for_admin(term, %{admin: admin, local: local, page: page, page_size: page_size}) do
758 term = String.trim_leading(term, "@")
759
760 local_paginated_query =
761 User
762 |> maybe_local_user_query(local)
763 |> paginate(page, page_size)
764
765 search_query = fts_search_subquery(term, local_paginated_query)
766
767 count =
768 term
769 |> fts_search_subquery()
770 |> maybe_local_user_query(local)
771 |> Repo.aggregate(:count, :id)
772
773 {:ok, do_search(search_query, admin), count}
774 end
775
776 @spec all_for_admin(number(), number()) :: {:ok, [Pleroma.User.t()], number()}
777 def all_for_admin(page, page_size) do
778 query = from(u in User, order_by: u.id)
779
780 paginated_query =
781 query
782 |> paginate(page, page_size)
783
784 count =
785 query
786 |> Repo.aggregate(:count, :id)
787
788 {:ok, Repo.all(paginated_query), count}
789 end
790
791 def search(query, resolve \\ false, for_user \\ nil) do
792 # Strip the beginning @ off if there is a query
793 query = String.trim_leading(query, "@")
794
795 if resolve, do: get_or_fetch(query)
796
797 fts_results = do_search(fts_search_subquery(query), for_user)
798
799 {:ok, trigram_results} =
800 Repo.transaction(fn ->
801 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
802 do_search(trigram_search_subquery(query), for_user)
803 end)
804
805 Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
806 end
807
808 defp do_search(subquery, for_user, options \\ []) do
809 q =
810 from(
811 s in subquery(subquery),
812 order_by: [desc: s.search_rank],
813 limit: ^(options[:limit] || 20)
814 )
815
816 results =
817 q
818 |> Repo.all()
819 |> Enum.filter(&(&1.search_rank > 0))
820
821 boost_search_results(results, for_user)
822 end
823
824 defp fts_search_subquery(term, query \\ User) do
825 processed_query =
826 term
827 |> String.replace(~r/\W+/, " ")
828 |> String.trim()
829 |> String.split()
830 |> Enum.map(&(&1 <> ":*"))
831 |> Enum.join(" | ")
832
833 from(
834 u in query,
835 select_merge: %{
836 search_rank:
837 fragment(
838 """
839 ts_rank_cd(
840 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
841 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
842 to_tsquery('simple', ?),
843 32
844 )
845 """,
846 u.nickname,
847 u.name,
848 ^processed_query
849 )
850 },
851 where:
852 fragment(
853 """
854 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
855 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
856 """,
857 u.nickname,
858 u.name,
859 ^processed_query
860 )
861 )
862 end
863
864 defp trigram_search_subquery(term) do
865 from(
866 u in User,
867 select_merge: %{
868 search_rank:
869 fragment(
870 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
871 ^term,
872 u.nickname,
873 u.name
874 )
875 },
876 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
877 )
878 end
879
880 defp boost_search_results(results, nil), do: results
881
882 defp boost_search_results(results, for_user) do
883 friends_ids = get_friends_ids(for_user)
884 followers_ids = get_followers_ids(for_user)
885
886 Enum.map(
887 results,
888 fn u ->
889 search_rank_coef =
890 cond do
891 u.id in friends_ids ->
892 1.2
893
894 u.id in followers_ids ->
895 1.1
896
897 true ->
898 1
899 end
900
901 Map.put(u, :search_rank, u.search_rank * search_rank_coef)
902 end
903 )
904 |> Enum.sort_by(&(-&1.search_rank))
905 end
906
907 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
908 Enum.map(
909 blocked_identifiers,
910 fn blocked_identifier ->
911 with %User{} = blocked <- get_or_fetch(blocked_identifier),
912 {:ok, blocker} <- block(blocker, blocked),
913 {:ok, _} <- ActivityPub.block(blocker, blocked) do
914 blocked
915 else
916 err ->
917 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
918 err
919 end
920 end
921 )
922 end
923
924 def mute(muter, %User{ap_id: ap_id}) do
925 info_cng =
926 muter.info
927 |> User.Info.add_to_mutes(ap_id)
928
929 cng =
930 change(muter)
931 |> put_embed(:info, info_cng)
932
933 update_and_set_cache(cng)
934 end
935
936 def unmute(muter, %{ap_id: ap_id}) do
937 info_cng =
938 muter.info
939 |> User.Info.remove_from_mutes(ap_id)
940
941 cng =
942 change(muter)
943 |> put_embed(:info, info_cng)
944
945 update_and_set_cache(cng)
946 end
947
948 def block(blocker, %User{ap_id: ap_id} = blocked) do
949 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
950 blocker =
951 if following?(blocker, blocked) do
952 {:ok, blocker, _} = unfollow(blocker, blocked)
953 blocker
954 else
955 blocker
956 end
957
958 if following?(blocked, blocker) do
959 unfollow(blocked, blocker)
960 end
961
962 info_cng =
963 blocker.info
964 |> User.Info.add_to_block(ap_id)
965
966 cng =
967 change(blocker)
968 |> put_embed(:info, info_cng)
969
970 update_and_set_cache(cng)
971 end
972
973 # helper to handle the block given only an actor's AP id
974 def block(blocker, %{ap_id: ap_id}) do
975 block(blocker, User.get_by_ap_id(ap_id))
976 end
977
978 def unblock(blocker, %{ap_id: ap_id}) do
979 info_cng =
980 blocker.info
981 |> User.Info.remove_from_block(ap_id)
982
983 cng =
984 change(blocker)
985 |> put_embed(:info, info_cng)
986
987 update_and_set_cache(cng)
988 end
989
990 def mutes?(nil, _), do: false
991 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
992
993 def blocks?(user, %{ap_id: ap_id}) do
994 blocks = user.info.blocks
995 domain_blocks = user.info.domain_blocks
996 %{host: host} = URI.parse(ap_id)
997
998 Enum.member?(blocks, ap_id) ||
999 Enum.any?(domain_blocks, fn domain ->
1000 host == domain
1001 end)
1002 end
1003
1004 def muted_users(user),
1005 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1006
1007 def blocked_users(user),
1008 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1009
1010 def block_domain(user, domain) do
1011 info_cng =
1012 user.info
1013 |> User.Info.add_to_domain_block(domain)
1014
1015 cng =
1016 change(user)
1017 |> put_embed(:info, info_cng)
1018
1019 update_and_set_cache(cng)
1020 end
1021
1022 def unblock_domain(user, domain) do
1023 info_cng =
1024 user.info
1025 |> User.Info.remove_from_domain_block(domain)
1026
1027 cng =
1028 change(user)
1029 |> put_embed(:info, info_cng)
1030
1031 update_and_set_cache(cng)
1032 end
1033
1034 def maybe_local_user_query(query, local) do
1035 if local, do: local_user_query(query), else: query
1036 end
1037
1038 def local_user_query(query \\ User) do
1039 from(
1040 u in query,
1041 where: u.local == true,
1042 where: not is_nil(u.nickname)
1043 )
1044 end
1045
1046 def active_local_user_query do
1047 from(
1048 u in local_user_query(),
1049 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1050 )
1051 end
1052
1053 def moderator_user_query do
1054 from(
1055 u in User,
1056 where: u.local == true,
1057 where: fragment("?->'is_moderator' @> 'true'", u.info)
1058 )
1059 end
1060
1061 def deactivate(%User{} = user, status \\ true) do
1062 info_cng = User.Info.set_activation_status(user.info, status)
1063
1064 cng =
1065 change(user)
1066 |> put_embed(:info, info_cng)
1067
1068 update_and_set_cache(cng)
1069 end
1070
1071 def delete(%User{} = user) do
1072 {:ok, user} = User.deactivate(user)
1073
1074 # Remove all relationships
1075 {:ok, followers} = User.get_followers(user)
1076
1077 followers
1078 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
1079
1080 {:ok, friends} = User.get_friends(user)
1081
1082 friends
1083 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
1084
1085 query = from(a in Activity, where: a.actor == ^user.ap_id)
1086
1087 Repo.all(query)
1088 |> Enum.each(fn activity ->
1089 case activity.data["type"] do
1090 "Create" ->
1091 ActivityPub.delete(Object.normalize(activity.data["object"]))
1092
1093 # TODO: Do something with likes, follows, repeats.
1094 _ ->
1095 "Doing nothing"
1096 end
1097 end)
1098
1099 {:ok, user}
1100 end
1101
1102 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1103 Pleroma.HTML.Scrubber.TwitterText
1104 end
1105
1106 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1107
1108 def html_filter_policy(_), do: @default_scrubbers
1109
1110 def get_or_fetch_by_ap_id(ap_id) do
1111 user = get_by_ap_id(ap_id)
1112
1113 if !is_nil(user) and !User.needs_update?(user) do
1114 user
1115 else
1116 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1117
1118 case ap_try do
1119 {:ok, user} ->
1120 user
1121
1122 _ ->
1123 case OStatus.make_user(ap_id) do
1124 {:ok, user} -> user
1125 _ -> {:error, "Could not fetch by AP id"}
1126 end
1127 end
1128 end
1129 end
1130
1131 def get_or_create_instance_user do
1132 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1133
1134 if user = get_by_ap_id(relay_uri) do
1135 user
1136 else
1137 changes =
1138 %User{info: %User.Info{}}
1139 |> cast(%{}, [:ap_id, :nickname, :local])
1140 |> put_change(:ap_id, relay_uri)
1141 |> put_change(:nickname, nil)
1142 |> put_change(:local, true)
1143 |> put_change(:follower_address, relay_uri <> "/followers")
1144
1145 {:ok, user} = Repo.insert(changes)
1146 user
1147 end
1148 end
1149
1150 # AP style
1151 def public_key_from_info(%{
1152 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1153 }) do
1154 key =
1155 public_key_pem
1156 |> :public_key.pem_decode()
1157 |> hd()
1158 |> :public_key.pem_entry_decode()
1159
1160 {:ok, key}
1161 end
1162
1163 # OStatus Magic Key
1164 def public_key_from_info(%{magic_key: magic_key}) do
1165 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1166 end
1167
1168 def get_public_key_for_ap_id(ap_id) do
1169 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1170 {:ok, public_key} <- public_key_from_info(user.info) do
1171 {:ok, public_key}
1172 else
1173 _ -> :error
1174 end
1175 end
1176
1177 defp blank?(""), do: nil
1178 defp blank?(n), do: n
1179
1180 def insert_or_update_user(data) do
1181 data =
1182 data
1183 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1184
1185 cs = User.remote_user_creation(data)
1186
1187 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1188 end
1189
1190 def ap_enabled?(%User{local: true}), do: true
1191 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1192 def ap_enabled?(_), do: false
1193
1194 @doc "Gets or fetch a user by uri or nickname."
1195 @spec get_or_fetch(String.t()) :: User.t()
1196 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1197 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1198
1199 # wait a period of time and return newest version of the User structs
1200 # this is because we have synchronous follow APIs and need to simulate them
1201 # with an async handshake
1202 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1203 with %User{} = a <- Repo.get(User, a.id),
1204 %User{} = b <- Repo.get(User, b.id) do
1205 {:ok, a, b}
1206 else
1207 _e ->
1208 :error
1209 end
1210 end
1211
1212 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1213 with :ok <- :timer.sleep(timeout),
1214 %User{} = a <- Repo.get(User, a.id),
1215 %User{} = b <- Repo.get(User, b.id) do
1216 {:ok, a, b}
1217 else
1218 _e ->
1219 :error
1220 end
1221 end
1222
1223 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1224 def parse_bio(nil, _user), do: ""
1225 def parse_bio(bio, _user) when bio == "", do: bio
1226
1227 def parse_bio(bio, user) do
1228 emoji =
1229 (user.info.source_data["tag"] || [])
1230 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1231 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1232 {String.trim(name, ":"), url}
1233 end)
1234
1235 bio
1236 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1237 |> elem(0)
1238 |> Formatter.emojify(emoji)
1239 end
1240
1241 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1242 Repo.transaction(fn ->
1243 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1244 end)
1245 end
1246
1247 def tag(nickname, tags) when is_binary(nickname),
1248 do: tag(User.get_by_nickname(nickname), tags)
1249
1250 def tag(%User{} = user, tags),
1251 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1252
1253 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1254 Repo.transaction(fn ->
1255 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1256 end)
1257 end
1258
1259 def untag(nickname, tags) when is_binary(nickname),
1260 do: untag(User.get_by_nickname(nickname), tags)
1261
1262 def untag(%User{} = user, tags),
1263 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1264
1265 defp update_tags(%User{} = user, new_tags) do
1266 {:ok, updated_user} =
1267 user
1268 |> change(%{tags: new_tags})
1269 |> update_and_set_cache()
1270
1271 updated_user
1272 end
1273
1274 def bookmark(%User{} = user, status_id) do
1275 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1276 update_bookmarks(user, bookmarks)
1277 end
1278
1279 def unbookmark(%User{} = user, status_id) do
1280 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1281 update_bookmarks(user, bookmarks)
1282 end
1283
1284 def update_bookmarks(%User{} = user, bookmarks) do
1285 user
1286 |> change(%{bookmarks: bookmarks})
1287 |> update_and_set_cache
1288 end
1289
1290 defp normalize_tags(tags) do
1291 [tags]
1292 |> List.flatten()
1293 |> Enum.map(&String.downcase(&1))
1294 end
1295
1296 defp local_nickname_regex() do
1297 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1298 @extended_local_nickname_regex
1299 else
1300 @strict_local_nickname_regex
1301 end
1302 end
1303
1304 def local_nickname(nickname_or_mention) do
1305 nickname_or_mention
1306 |> full_nickname()
1307 |> String.split("@")
1308 |> hd()
1309 end
1310
1311 def full_nickname(nickname_or_mention),
1312 do: String.trim_leading(nickname_or_mention, "@")
1313
1314 def error_user(ap_id) do
1315 %User{
1316 name: ap_id,
1317 ap_id: ap_id,
1318 info: %User.Info{},
1319 nickname: "erroruser@example.com",
1320 inserted_at: NaiveDateTime.utc_now()
1321 }
1322 end
1323
1324 def all_superusers do
1325 from(
1326 u in User,
1327 where: u.local == true,
1328 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1329 )
1330 |> Repo.all()
1331 end
1332
1333 defp paginate(query, page, page_size) do
1334 from(u in query,
1335 limit: ^page_size,
1336 offset: ^((page - 1) * page_size)
1337 )
1338 end
1339 end