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