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