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