Add logic for keeping follow_request_count up-to-date on the `follow`,
[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 |> put_change(:following, [followers])
241 |> put_change(:follower_address, followers)
242 else
243 changeset
244 end
245 end
246
247 defp autofollow_users(user) do
248 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
249
250 autofollowed_users =
251 from(u in User,
252 where: u.local == true,
253 where: u.nickname in ^candidates
254 )
255 |> Repo.all()
256
257 follow_all(user, autofollowed_users)
258 end
259
260 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
261 def register(%Ecto.Changeset{} = changeset) do
262 with {:ok, user} <- Repo.insert(changeset),
263 {:ok, user} <- autofollow_users(user),
264 {:ok, _} <- try_send_confirmation_email(user) do
265 {:ok, user}
266 end
267 end
268
269 def try_send_confirmation_email(%User{} = user) do
270 if user.info.confirmation_pending &&
271 Pleroma.Config.get([:instance, :account_activation_required]) do
272 user
273 |> Pleroma.UserEmail.account_confirmation_email()
274 |> Pleroma.Mailer.deliver()
275 else
276 {:ok, :noop}
277 end
278 end
279
280 def needs_update?(%User{local: true}), do: false
281
282 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
283
284 def needs_update?(%User{local: false} = user) do
285 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400
286 end
287
288 def needs_update?(_), do: true
289
290 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
291 {:ok, follower}
292 end
293
294 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
295 follow(follower, followed)
296 end
297
298 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
299 if not User.ap_enabled?(followed) do
300 follow(follower, followed)
301 else
302 {:ok, follower}
303 end
304 end
305
306 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
307 if not following?(follower, followed) do
308 follow(follower, followed)
309 else
310 {:ok, follower}
311 end
312 end
313
314 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
315 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
316 def follow_all(follower, followeds) do
317 followed_addresses =
318 followeds
319 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
320 |> Enum.map(fn %{follower_address: fa} -> fa end)
321
322 q =
323 from(u in User,
324 where: u.id == ^follower.id,
325 update: [
326 set: [
327 following:
328 fragment(
329 "array(select distinct unnest (array_cat(?, ?)))",
330 u.following,
331 ^followed_addresses
332 )
333 ]
334 ]
335 )
336
337 {1, [follower]} = Repo.update_all(q, [], returning: true)
338
339 Enum.each(followeds, fn followed ->
340 update_follower_count(followed)
341 end)
342
343 set_cache(follower)
344 end
345
346 def follow(%User{} = follower, %User{info: info} = followed) do
347 user_config = Application.get_env(:pleroma, :user)
348 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
349
350 ap_followers = followed.follower_address
351
352 cond do
353 following?(follower, followed) or info.deactivated ->
354 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
355
356 deny_follow_blocked and blocks?(followed, follower) ->
357 {:error, "Could not follow user: #{followed.nickname} blocked you."}
358
359 true ->
360 if !followed.local && follower.local && !ap_enabled?(followed) do
361 Websub.subscribe(follower, followed)
362 end
363
364 q =
365 from(u in User,
366 where: u.id == ^follower.id,
367 update: [push: [following: ^ap_followers]]
368 )
369
370 {1, [follower]} = Repo.update_all(q, [], returning: true)
371
372 {:ok, _} = update_follower_count(followed)
373
374 set_cache(follower)
375 end
376 end
377
378 def unfollow(%User{} = follower, %User{} = followed) do
379 ap_followers = followed.follower_address
380
381 if following?(follower, followed) and follower.ap_id != followed.ap_id do
382 q =
383 from(u in User,
384 where: u.id == ^follower.id,
385 update: [pull: [following: ^ap_followers]]
386 )
387
388 {1, [follower]} = Repo.update_all(q, [], returning: true)
389
390 {:ok, followed} = update_follower_count(followed)
391
392 set_cache(follower)
393
394 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
395 else
396 {:error, "Not subscribed!"}
397 end
398 end
399
400 @spec following?(User.t(), User.t()) :: boolean
401 def following?(%User{} = follower, %User{} = followed) do
402 Enum.member?(follower.following, followed.follower_address)
403 end
404
405 def follow_import(%User{} = follower, followed_identifiers)
406 when is_list(followed_identifiers) do
407 Enum.map(
408 followed_identifiers,
409 fn followed_identifier ->
410 with %User{} = followed <- get_or_fetch(followed_identifier),
411 {:ok, follower} <- maybe_direct_follow(follower, followed),
412 {:ok, _} <- ActivityPub.follow(follower, followed) do
413 followed
414 else
415 err ->
416 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
417 err
418 end
419 end
420 )
421 end
422
423 def locked?(%User{} = user) do
424 user.info.locked || false
425 end
426
427 def get_by_id(id) do
428 Repo.get_by(User, id: id)
429 end
430
431 def get_by_ap_id(ap_id) do
432 Repo.get_by(User, ap_id: ap_id)
433 end
434
435 # 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
436 def get_by_guessed_nickname(ap_id) do
437 domain = URI.parse(ap_id).host
438 name = List.last(String.split(ap_id, "/"))
439 nickname = "#{name}@#{domain}"
440
441 get_by_nickname(nickname)
442 end
443
444 def set_cache(user) do
445 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
446 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
447 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
448 {:ok, user}
449 end
450
451 def update_and_set_cache(changeset) do
452 with {:ok, user} <- Repo.update(changeset) do
453 set_cache(user)
454 else
455 e -> e
456 end
457 end
458
459 def invalidate_cache(user) do
460 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
461 Cachex.del(:user_cache, "nickname:#{user.nickname}")
462 Cachex.del(:user_cache, "user_info:#{user.id}")
463 end
464
465 def get_cached_by_ap_id(ap_id) do
466 key = "ap_id:#{ap_id}"
467 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
468 end
469
470 def get_cached_by_id(id) do
471 key = "id:#{id}"
472
473 ap_id =
474 Cachex.fetch!(:user_cache, key, fn _ ->
475 user = get_by_id(id)
476
477 if user do
478 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
479 {:commit, user.ap_id}
480 else
481 {:ignore, ""}
482 end
483 end)
484
485 get_cached_by_ap_id(ap_id)
486 end
487
488 def get_cached_by_nickname(nickname) do
489 key = "nickname:#{nickname}"
490 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
491 end
492
493 def get_cached_by_nickname_or_id(nickname_or_id) do
494 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
495 end
496
497 def get_by_nickname(nickname) do
498 Repo.get_by(User, nickname: nickname) ||
499 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
500 Repo.get_by(User, nickname: local_nickname(nickname))
501 end
502 end
503
504 def get_by_nickname_or_email(nickname_or_email) do
505 case user = Repo.get_by(User, nickname: nickname_or_email) do
506 %User{} -> user
507 nil -> Repo.get_by(User, email: nickname_or_email)
508 end
509 end
510
511 def get_cached_user_info(user) do
512 key = "user_info:#{user.id}"
513 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
514 end
515
516 def fetch_by_nickname(nickname) do
517 ap_try = ActivityPub.make_user_from_nickname(nickname)
518
519 case ap_try do
520 {:ok, user} -> {:ok, user}
521 _ -> OStatus.make_user(nickname)
522 end
523 end
524
525 def get_or_fetch_by_nickname(nickname) do
526 with %User{} = user <- get_by_nickname(nickname) do
527 user
528 else
529 _e ->
530 with [_nick, _domain] <- String.split(nickname, "@"),
531 {:ok, user} <- fetch_by_nickname(nickname) do
532 user
533 else
534 _e -> nil
535 end
536 end
537 end
538
539 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
540 from(
541 u in User,
542 where: fragment("? <@ ?", ^[follower_address], u.following),
543 where: u.id != ^id
544 )
545 end
546
547 def get_followers_query(user, page) do
548 from(
549 u in get_followers_query(user, nil),
550 limit: 20,
551 offset: ^((page - 1) * 20)
552 )
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(
579 u in get_friends_query(user, nil),
580 limit: 20,
581 offset: ^((page - 1) * 20)
582 )
583 end
584
585 def get_friends_query(user), do: get_friends_query(user, nil)
586
587 def get_friends(user, page \\ nil) do
588 q = get_friends_query(user, page)
589
590 {:ok, Repo.all(q)}
591 end
592
593 def get_friends_ids(user, page \\ nil) do
594 q = get_friends_query(user, page)
595
596 Repo.all(from(u in q, select: u.id))
597 end
598
599 def get_follow_requests_query(%User{} = user) do
600 from(
601 a in Activity,
602 where:
603 fragment(
604 "? ->> 'type' = 'Follow'",
605 a.data
606 ),
607 where:
608 fragment(
609 "? ->> 'state' = 'pending'",
610 a.data
611 ),
612 where:
613 fragment(
614 "? @> ?",
615 a.data,
616 ^%{"object" => user.ap_id}
617 )
618 )
619 end
620
621 def update_follow_request_count(%User{} = user) do
622 subquery =
623 user
624 |> User.get_follow_requests_query()
625 |> select([a], %{count: count(a.id)})
626
627 User
628 |> where(id: ^user.id)
629 |> join(:inner, [u], s in subquery(subquery))
630 |> update([u, s],
631 set: [
632 info:
633 fragment(
634 "jsonb_set(?, '{follow_request_count}', ?::varchar::jsonb, true)",
635 u.info,
636 s.count
637 )
638 ]
639 )
640 |> Repo.update_all([], returning: true)
641 |> case do
642 {1, [user]} -> {:ok, user}
643 _ -> {:error, user}
644 end
645 end
646
647 def get_follow_requests(%User{} = user) do
648 q = get_follow_requests_query(user)
649 reqs = Repo.all(q)
650
651 users =
652 Enum.map(reqs, fn req -> req.actor end)
653 |> Enum.uniq()
654 |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
655 |> Enum.filter(fn u -> !is_nil(u) end)
656 |> Enum.filter(fn u -> !following?(u, user) end)
657
658 {:ok, users}
659 end
660
661 def increase_note_count(%User{} = user) do
662 info_cng = User.Info.add_to_note_count(user.info, 1)
663
664 cng =
665 change(user)
666 |> put_embed(:info, info_cng)
667
668 update_and_set_cache(cng)
669 end
670
671 def decrease_note_count(%User{} = user) do
672 info_cng = User.Info.add_to_note_count(user.info, -1)
673
674 cng =
675 change(user)
676 |> put_embed(:info, info_cng)
677
678 update_and_set_cache(cng)
679 end
680
681 def update_note_count(%User{} = user) do
682 note_count_query =
683 from(
684 a in Object,
685 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
686 select: count(a.id)
687 )
688
689 note_count = Repo.one(note_count_query)
690
691 info_cng = User.Info.set_note_count(user.info, note_count)
692
693 cng =
694 change(user)
695 |> put_embed(:info, info_cng)
696
697 update_and_set_cache(cng)
698 end
699
700 def update_follower_count(%User{} = user) do
701 follower_count_query =
702 from(
703 u in User,
704 where: ^user.follower_address in u.following,
705 where: u.id != ^user.id,
706 select: count(u.id)
707 )
708
709 follower_count = Repo.one(follower_count_query)
710
711 info_cng =
712 user.info
713 |> User.Info.set_follower_count(follower_count)
714
715 cng =
716 change(user)
717 |> put_embed(:info, info_cng)
718
719 update_and_set_cache(cng)
720 end
721
722 def get_users_from_set_query(ap_ids, false) do
723 from(
724 u in User,
725 where: u.ap_id in ^ap_ids
726 )
727 end
728
729 def get_users_from_set_query(ap_ids, true) do
730 query = get_users_from_set_query(ap_ids, false)
731
732 from(
733 u in query,
734 where: u.local == true
735 )
736 end
737
738 def get_users_from_set(ap_ids, local_only \\ true) do
739 get_users_from_set_query(ap_ids, local_only)
740 |> Repo.all()
741 end
742
743 def get_recipients_from_activity(%Activity{recipients: to}) do
744 query =
745 from(
746 u in User,
747 where: u.ap_id in ^to,
748 or_where: fragment("? && ?", u.following, ^to)
749 )
750
751 query = from(u in query, where: u.local == true)
752
753 Repo.all(query)
754 end
755
756 def search(query, resolve \\ false, for_user \\ nil) do
757 # Strip the beginning @ off if there is a query
758 query = String.trim_leading(query, "@")
759
760 if resolve, do: User.get_or_fetch_by_nickname(query)
761
762 fts_results = do_search(fts_search_subquery(query), for_user)
763
764 {:ok, trigram_results} =
765 Repo.transaction(fn ->
766 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
767 do_search(trigram_search_subquery(query), for_user)
768 end)
769
770 Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
771 end
772
773 defp do_search(subquery, for_user, options \\ []) do
774 q =
775 from(
776 s in subquery(subquery),
777 order_by: [desc: s.search_rank],
778 limit: ^(options[:limit] || 20)
779 )
780
781 results =
782 q
783 |> Repo.all()
784 |> Enum.filter(&(&1.search_rank > 0))
785
786 boost_search_results(results, for_user)
787 end
788
789 defp fts_search_subquery(query) do
790 processed_query =
791 query
792 |> String.replace(~r/\W+/, " ")
793 |> String.trim()
794 |> String.split()
795 |> Enum.map(&(&1 <> ":*"))
796 |> Enum.join(" | ")
797
798 from(
799 u in User,
800 select_merge: %{
801 search_rank:
802 fragment(
803 """
804 ts_rank_cd(
805 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
806 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
807 to_tsquery('simple', ?),
808 32
809 )
810 """,
811 u.nickname,
812 u.name,
813 ^processed_query
814 )
815 },
816 where:
817 fragment(
818 """
819 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
820 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
821 """,
822 u.nickname,
823 u.name,
824 ^processed_query
825 )
826 )
827 end
828
829 defp trigram_search_subquery(query) do
830 from(
831 u in User,
832 select_merge: %{
833 search_rank:
834 fragment(
835 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
836 ^query,
837 u.nickname,
838 u.name
839 )
840 },
841 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
842 )
843 end
844
845 defp boost_search_results(results, nil), do: results
846
847 defp boost_search_results(results, for_user) do
848 friends_ids = get_friends_ids(for_user)
849 followers_ids = get_followers_ids(for_user)
850
851 Enum.map(
852 results,
853 fn u ->
854 search_rank_coef =
855 cond do
856 u.id in friends_ids ->
857 1.2
858
859 u.id in followers_ids ->
860 1.1
861
862 true ->
863 1
864 end
865
866 Map.put(u, :search_rank, u.search_rank * search_rank_coef)
867 end
868 )
869 |> Enum.sort_by(&(-&1.search_rank))
870 end
871
872 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
873 Enum.map(
874 blocked_identifiers,
875 fn blocked_identifier ->
876 with %User{} = blocked <- get_or_fetch(blocked_identifier),
877 {:ok, blocker} <- block(blocker, blocked),
878 {:ok, _} <- ActivityPub.block(blocker, blocked) do
879 blocked
880 else
881 err ->
882 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
883 err
884 end
885 end
886 )
887 end
888
889 def block(blocker, %User{ap_id: ap_id} = blocked) do
890 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
891 blocker =
892 if following?(blocker, blocked) do
893 {:ok, blocker, _} = unfollow(blocker, blocked)
894 blocker
895 else
896 blocker
897 end
898
899 if following?(blocked, blocker) do
900 unfollow(blocked, blocker)
901 end
902
903 info_cng =
904 blocker.info
905 |> User.Info.add_to_block(ap_id)
906
907 cng =
908 change(blocker)
909 |> put_embed(:info, info_cng)
910
911 update_and_set_cache(cng)
912 end
913
914 # helper to handle the block given only an actor's AP id
915 def block(blocker, %{ap_id: ap_id}) do
916 block(blocker, User.get_by_ap_id(ap_id))
917 end
918
919 def unblock(blocker, %{ap_id: ap_id}) do
920 info_cng =
921 blocker.info
922 |> User.Info.remove_from_block(ap_id)
923
924 cng =
925 change(blocker)
926 |> put_embed(:info, info_cng)
927
928 update_and_set_cache(cng)
929 end
930
931 def blocks?(user, %{ap_id: ap_id}) do
932 blocks = user.info.blocks
933 domain_blocks = user.info.domain_blocks
934 %{host: host} = URI.parse(ap_id)
935
936 Enum.member?(blocks, ap_id) ||
937 Enum.any?(domain_blocks, fn domain ->
938 host == domain
939 end)
940 end
941
942 def blocked_users(user),
943 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
944
945 def block_domain(user, domain) do
946 info_cng =
947 user.info
948 |> User.Info.add_to_domain_block(domain)
949
950 cng =
951 change(user)
952 |> put_embed(:info, info_cng)
953
954 update_and_set_cache(cng)
955 end
956
957 def unblock_domain(user, domain) do
958 info_cng =
959 user.info
960 |> User.Info.remove_from_domain_block(domain)
961
962 cng =
963 change(user)
964 |> put_embed(:info, info_cng)
965
966 update_and_set_cache(cng)
967 end
968
969 def local_user_query do
970 from(
971 u in User,
972 where: u.local == true,
973 where: not is_nil(u.nickname)
974 )
975 end
976
977 def active_local_user_query do
978 from(
979 u in local_user_query(),
980 where: fragment("not (?->'deactivated' @> 'true')", u.info)
981 )
982 end
983
984 def moderator_user_query do
985 from(
986 u in User,
987 where: u.local == true,
988 where: fragment("?->'is_moderator' @> 'true'", u.info)
989 )
990 end
991
992 def deactivate(%User{} = user, status \\ true) do
993 info_cng = User.Info.set_activation_status(user.info, status)
994
995 cng =
996 change(user)
997 |> put_embed(:info, info_cng)
998
999 update_and_set_cache(cng)
1000 end
1001
1002 def delete(%User{} = user) do
1003 {:ok, user} = User.deactivate(user)
1004
1005 # Remove all relationships
1006 {:ok, followers} = User.get_followers(user)
1007
1008 followers
1009 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
1010
1011 {:ok, friends} = User.get_friends(user)
1012
1013 friends
1014 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
1015
1016 query = from(a in Activity, where: a.actor == ^user.ap_id)
1017
1018 Repo.all(query)
1019 |> Enum.each(fn activity ->
1020 case activity.data["type"] do
1021 "Create" ->
1022 ActivityPub.delete(Object.normalize(activity.data["object"]))
1023
1024 # TODO: Do something with likes, follows, repeats.
1025 _ ->
1026 "Doing nothing"
1027 end
1028 end)
1029
1030 {:ok, user}
1031 end
1032
1033 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1034 Pleroma.HTML.Scrubber.TwitterText
1035 end
1036
1037 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1038
1039 def html_filter_policy(_), do: @default_scrubbers
1040
1041 def get_or_fetch_by_ap_id(ap_id) do
1042 user = get_by_ap_id(ap_id)
1043
1044 if !is_nil(user) and !User.needs_update?(user) do
1045 user
1046 else
1047 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1048
1049 case ap_try do
1050 {:ok, user} ->
1051 user
1052
1053 _ ->
1054 case OStatus.make_user(ap_id) do
1055 {:ok, user} -> user
1056 _ -> {:error, "Could not fetch by AP id"}
1057 end
1058 end
1059 end
1060 end
1061
1062 def get_or_create_instance_user do
1063 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1064
1065 if user = get_by_ap_id(relay_uri) do
1066 user
1067 else
1068 changes =
1069 %User{info: %User.Info{}}
1070 |> cast(%{}, [:ap_id, :nickname, :local])
1071 |> put_change(:ap_id, relay_uri)
1072 |> put_change(:nickname, nil)
1073 |> put_change(:local, true)
1074 |> put_change(:follower_address, relay_uri <> "/followers")
1075
1076 {:ok, user} = Repo.insert(changes)
1077 user
1078 end
1079 end
1080
1081 # AP style
1082 def public_key_from_info(%{
1083 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1084 }) do
1085 key =
1086 public_key_pem
1087 |> :public_key.pem_decode()
1088 |> hd()
1089 |> :public_key.pem_entry_decode()
1090
1091 {:ok, key}
1092 end
1093
1094 # OStatus Magic Key
1095 def public_key_from_info(%{magic_key: magic_key}) do
1096 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1097 end
1098
1099 def get_public_key_for_ap_id(ap_id) do
1100 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1101 {:ok, public_key} <- public_key_from_info(user.info) do
1102 {:ok, public_key}
1103 else
1104 _ -> :error
1105 end
1106 end
1107
1108 defp blank?(""), do: nil
1109 defp blank?(n), do: n
1110
1111 def insert_or_update_user(data) do
1112 data =
1113 data
1114 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1115
1116 cs = User.remote_user_creation(data)
1117
1118 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1119 end
1120
1121 def ap_enabled?(%User{local: true}), do: true
1122 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1123 def ap_enabled?(_), do: false
1124
1125 @doc "Gets or fetch a user by uri or nickname."
1126 @spec get_or_fetch(String.t()) :: User.t()
1127 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1128 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1129
1130 # wait a period of time and return newest version of the User structs
1131 # this is because we have synchronous follow APIs and need to simulate them
1132 # with an async handshake
1133 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1134 with %User{} = a <- Repo.get(User, a.id),
1135 %User{} = b <- Repo.get(User, b.id) do
1136 {:ok, a, b}
1137 else
1138 _e ->
1139 :error
1140 end
1141 end
1142
1143 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1144 with :ok <- :timer.sleep(timeout),
1145 %User{} = a <- Repo.get(User, a.id),
1146 %User{} = b <- Repo.get(User, b.id) do
1147 {:ok, a, b}
1148 else
1149 _e ->
1150 :error
1151 end
1152 end
1153
1154 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1155 def parse_bio(nil, _user), do: ""
1156 def parse_bio(bio, _user) when bio == "", do: bio
1157
1158 def parse_bio(bio, user) do
1159 mentions = Formatter.parse_mentions(bio)
1160 tags = Formatter.parse_tags(bio)
1161
1162 emoji =
1163 (user.info.source_data["tag"] || [])
1164 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1165 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1166 {String.trim(name, ":"), url}
1167 end)
1168
1169 bio
1170 |> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full])
1171 |> Formatter.emojify(emoji)
1172 end
1173
1174 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1175 Repo.transaction(fn ->
1176 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1177 end)
1178 end
1179
1180 def tag(nickname, tags) when is_binary(nickname),
1181 do: tag(User.get_by_nickname(nickname), tags)
1182
1183 def tag(%User{} = user, tags),
1184 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1185
1186 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1187 Repo.transaction(fn ->
1188 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1189 end)
1190 end
1191
1192 def untag(nickname, tags) when is_binary(nickname),
1193 do: untag(User.get_by_nickname(nickname), tags)
1194
1195 def untag(%User{} = user, tags),
1196 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1197
1198 defp update_tags(%User{} = user, new_tags) do
1199 {:ok, updated_user} =
1200 user
1201 |> change(%{tags: new_tags})
1202 |> Repo.update()
1203
1204 updated_user
1205 end
1206
1207 def bookmark(%User{} = user, status_id) do
1208 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1209 update_bookmarks(user, bookmarks)
1210 end
1211
1212 def unbookmark(%User{} = user, status_id) do
1213 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1214 update_bookmarks(user, bookmarks)
1215 end
1216
1217 def update_bookmarks(%User{} = user, bookmarks) do
1218 user
1219 |> change(%{bookmarks: bookmarks})
1220 |> update_and_set_cache
1221 end
1222
1223 defp normalize_tags(tags) do
1224 [tags]
1225 |> List.flatten()
1226 |> Enum.map(&String.downcase(&1))
1227 end
1228
1229 defp local_nickname_regex() do
1230 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1231 @extended_local_nickname_regex
1232 else
1233 @strict_local_nickname_regex
1234 end
1235 end
1236
1237 def local_nickname(nickname_or_mention) do
1238 nickname_or_mention
1239 |> full_nickname()
1240 |> String.split("@")
1241 |> hd()
1242 end
1243
1244 def full_nickname(nickname_or_mention),
1245 do: String.trim_leading(nickname_or_mention, "@")
1246
1247 def error_user(ap_id) do
1248 %User{
1249 name: ap_id,
1250 ap_id: ap_id,
1251 info: %User.Info{},
1252 nickname: "erroruser@example.com",
1253 inserted_at: NaiveDateTime.utc_now()
1254 }
1255 end
1256 end