Merge branch 'security/fix-html-class-scrubbing' into 'develop'
[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 Comeonin.Pbkdf2
12 alias Pleroma.Activity
13 alias Pleroma.Formatter
14 alias Pleroma.Notification
15 alias Pleroma.Object
16 alias Pleroma.Registration
17 alias Pleroma.Repo
18 alias Pleroma.User
19 alias Pleroma.Web
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Utils
22 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
23 alias Pleroma.Web.OAuth
24 alias Pleroma.Web.OStatus
25 alias Pleroma.Web.RelMe
26 alias Pleroma.Web.Websub
27
28 require Logger
29
30 @type t :: %__MODULE__{}
31
32 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
33
34 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
35 @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])?)*$/
36
37 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
38 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
39
40 schema "users" do
41 field(:bio, :string)
42 field(:email, :string)
43 field(:name, :string)
44 field(:nickname, :string)
45 field(:password_hash, :string)
46 field(:password, :string, virtual: true)
47 field(:password_confirmation, :string, virtual: true)
48 field(:following, {:array, :string}, default: [])
49 field(:ap_id, :string)
50 field(:avatar, :map)
51 field(:local, :boolean, default: true)
52 field(:follower_address, :string)
53 field(:search_rank, :float, virtual: true)
54 field(:search_type, :integer, virtual: true)
55 field(:tags, {:array, :string}, default: [])
56 field(:bookmarks, {:array, :string}, default: [])
57 field(:last_refreshed_at, :naive_datetime_usec)
58 has_many(:notifications, Notification)
59 has_many(:registrations, Registration)
60 embeds_one(:info, Pleroma.User.Info)
61
62 timestamps()
63 end
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?(%User{}), do: true
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, options \\ []) do
85 case user.avatar do
86 %{"url" => [%{"href" => href} | _]} -> href
87 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
88 end
89 end
90
91 def banner_url(user, options \\ []) do
92 case user.info.banner do
93 %{"url" => [%{"href" => href} | _]} -> href
94 _ -> !options[:no_default] && "#{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{follower_address: fa}) when is_binary(fa), do: fa
107 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
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([: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 changeset =
233 if opts[:external] do
234 changeset
235 else
236 validate_required(changeset, [:email])
237 end
238
239 if changeset.valid? do
240 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
241 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
242 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
243
244 changeset
245 |> put_change(:password_hash, hashed)
246 |> put_change(:ap_id, ap_id)
247 |> unique_constraint(:ap_id)
248 |> put_change(:following, [followers])
249 |> put_change(:follower_address, followers)
250 else
251 changeset
252 end
253 end
254
255 defp autofollow_users(user) do
256 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
257
258 autofollowed_users =
259 from(u in User,
260 where: u.local == true,
261 where: u.nickname in ^candidates
262 )
263 |> Repo.all()
264
265 follow_all(user, autofollowed_users)
266 end
267
268 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
269 def register(%Ecto.Changeset{} = changeset) do
270 with {:ok, user} <- Repo.insert(changeset),
271 {:ok, user} <- autofollow_users(user),
272 {:ok, user} <- set_cache(user),
273 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
274 {:ok, _} <- try_send_confirmation_email(user) do
275 {:ok, user}
276 end
277 end
278
279 def try_send_confirmation_email(%User{} = user) do
280 if user.info.confirmation_pending &&
281 Pleroma.Config.get([:instance, :account_activation_required]) do
282 user
283 |> Pleroma.Emails.UserEmail.account_confirmation_email()
284 |> Pleroma.Emails.Mailer.deliver_async()
285
286 {:ok, :enqueued}
287 else
288 {:ok, :noop}
289 end
290 end
291
292 def needs_update?(%User{local: true}), do: false
293
294 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
295
296 def needs_update?(%User{local: false} = user) do
297 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
298 end
299
300 def needs_update?(_), do: true
301
302 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
303 {:ok, follower}
304 end
305
306 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
307 follow(follower, followed)
308 end
309
310 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
311 if not User.ap_enabled?(followed) do
312 follow(follower, followed)
313 else
314 {:ok, follower}
315 end
316 end
317
318 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
319 if not following?(follower, followed) do
320 follow(follower, followed)
321 else
322 {:ok, follower}
323 end
324 end
325
326 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
327 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
328 def follow_all(follower, followeds) do
329 followed_addresses =
330 followeds
331 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
332 |> Enum.map(fn %{follower_address: fa} -> fa end)
333
334 q =
335 from(u in User,
336 where: u.id == ^follower.id,
337 update: [
338 set: [
339 following:
340 fragment(
341 "array(select distinct unnest (array_cat(?, ?)))",
342 u.following,
343 ^followed_addresses
344 )
345 ]
346 ],
347 select: u
348 )
349
350 {1, [follower]} = Repo.update_all(q, [])
351
352 Enum.each(followeds, fn followed ->
353 update_follower_count(followed)
354 end)
355
356 set_cache(follower)
357 end
358
359 def follow(%User{} = follower, %User{info: info} = followed) do
360 user_config = Application.get_env(:pleroma, :user)
361 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
362
363 ap_followers = followed.follower_address
364
365 cond do
366 following?(follower, followed) or info.deactivated ->
367 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
368
369 deny_follow_blocked and blocks?(followed, follower) ->
370 {:error, "Could not follow user: #{followed.nickname} blocked you."}
371
372 true ->
373 if !followed.local && follower.local && !ap_enabled?(followed) do
374 Websub.subscribe(follower, followed)
375 end
376
377 q =
378 from(u in User,
379 where: u.id == ^follower.id,
380 update: [push: [following: ^ap_followers]],
381 select: u
382 )
383
384 {1, [follower]} = Repo.update_all(q, [])
385
386 {:ok, _} = update_follower_count(followed)
387
388 set_cache(follower)
389 end
390 end
391
392 def unfollow(%User{} = follower, %User{} = followed) do
393 ap_followers = followed.follower_address
394
395 if following?(follower, followed) and follower.ap_id != followed.ap_id do
396 q =
397 from(u in User,
398 where: u.id == ^follower.id,
399 update: [pull: [following: ^ap_followers]],
400 select: u
401 )
402
403 {1, [follower]} = Repo.update_all(q, [])
404
405 {:ok, followed} = update_follower_count(followed)
406
407 set_cache(follower)
408
409 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
410 else
411 {:error, "Not subscribed!"}
412 end
413 end
414
415 @spec following?(User.t(), User.t()) :: boolean
416 def following?(%User{} = follower, %User{} = followed) do
417 Enum.member?(follower.following, followed.follower_address)
418 end
419
420 def follow_import(%User{} = follower, followed_identifiers)
421 when is_list(followed_identifiers) do
422 Enum.map(
423 followed_identifiers,
424 fn followed_identifier ->
425 with %User{} = followed <- get_or_fetch(followed_identifier),
426 {:ok, follower} <- maybe_direct_follow(follower, followed),
427 {:ok, _} <- ActivityPub.follow(follower, followed) do
428 followed
429 else
430 err ->
431 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
432 err
433 end
434 end
435 )
436 end
437
438 def locked?(%User{} = user) do
439 user.info.locked || false
440 end
441
442 def get_by_id(id) do
443 Repo.get_by(User, id: id)
444 end
445
446 def get_by_ap_id(ap_id) do
447 Repo.get_by(User, ap_id: ap_id)
448 end
449
450 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
451 # of the ap_id and the domain and tries to get that user
452 def get_by_guessed_nickname(ap_id) do
453 domain = URI.parse(ap_id).host
454 name = List.last(String.split(ap_id, "/"))
455 nickname = "#{name}@#{domain}"
456
457 get_cached_by_nickname(nickname)
458 end
459
460 def set_cache({:ok, user}), do: set_cache(user)
461 def set_cache({:error, err}), do: {:error, err}
462
463 def set_cache(%User{} = user) do
464 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
465 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
466 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
467 {:ok, user}
468 end
469
470 def update_and_set_cache(changeset) do
471 with {:ok, user} <- Repo.update(changeset) do
472 set_cache(user)
473 else
474 e -> e
475 end
476 end
477
478 def invalidate_cache(user) do
479 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
480 Cachex.del(:user_cache, "nickname:#{user.nickname}")
481 Cachex.del(:user_cache, "user_info:#{user.id}")
482 end
483
484 def get_cached_by_ap_id(ap_id) do
485 key = "ap_id:#{ap_id}"
486 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
487 end
488
489 def get_cached_by_id(id) do
490 key = "id:#{id}"
491
492 ap_id =
493 Cachex.fetch!(:user_cache, key, fn _ ->
494 user = get_by_id(id)
495
496 if user do
497 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
498 {:commit, user.ap_id}
499 else
500 {:ignore, ""}
501 end
502 end)
503
504 get_cached_by_ap_id(ap_id)
505 end
506
507 def get_cached_by_nickname(nickname) do
508 key = "nickname:#{nickname}"
509 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
510 end
511
512 def get_cached_by_nickname_or_id(nickname_or_id) do
513 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
514 end
515
516 def get_by_nickname(nickname) do
517 Repo.get_by(User, nickname: nickname) ||
518 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
519 Repo.get_by(User, nickname: local_nickname(nickname))
520 end
521 end
522
523 def get_by_email(email), do: Repo.get_by(User, email: email)
524
525 def get_by_nickname_or_email(nickname_or_email) do
526 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
527 end
528
529 def get_cached_user_info(user) do
530 key = "user_info:#{user.id}"
531 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
532 end
533
534 def fetch_by_nickname(nickname) do
535 ap_try = ActivityPub.make_user_from_nickname(nickname)
536
537 case ap_try do
538 {:ok, user} -> {:ok, user}
539 _ -> OStatus.make_user(nickname)
540 end
541 end
542
543 def get_or_fetch_by_nickname(nickname) do
544 with %User{} = user <- get_by_nickname(nickname) do
545 user
546 else
547 _e ->
548 with [_nick, _domain] <- String.split(nickname, "@"),
549 {:ok, user} <- fetch_by_nickname(nickname) do
550 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
551 # TODO turn into job
552 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
553 end
554
555 user
556 else
557 _e -> nil
558 end
559 end
560 end
561
562 @doc "Fetch some posts when the user has just been federated with"
563 def fetch_initial_posts(user) do
564 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
565
566 Enum.each(
567 # Insert all the posts in reverse order, so they're in the right order on the timeline
568 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
569 &Pleroma.Web.Federator.incoming_ap_doc/1
570 )
571 end
572
573 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
574 from(
575 u in User,
576 where: fragment("? <@ ?", ^[follower_address], u.following),
577 where: u.id != ^id
578 )
579 end
580
581 def get_followers_query(user, page) do
582 from(u in get_followers_query(user, nil))
583 |> paginate(page, 20)
584 end
585
586 def get_followers_query(user), do: get_followers_query(user, nil)
587
588 def get_followers(user, page \\ nil) do
589 q = get_followers_query(user, page)
590
591 {:ok, Repo.all(q)}
592 end
593
594 def get_followers_ids(user, page \\ nil) do
595 q = get_followers_query(user, page)
596
597 Repo.all(from(u in q, select: u.id))
598 end
599
600 def get_friends_query(%User{id: id, following: following}, nil) do
601 from(
602 u in User,
603 where: u.follower_address in ^following,
604 where: u.id != ^id
605 )
606 end
607
608 def get_friends_query(user, page) do
609 from(u in get_friends_query(user, nil))
610 |> paginate(page, 20)
611 end
612
613 def get_friends_query(user), do: get_friends_query(user, nil)
614
615 def get_friends(user, page \\ nil) do
616 q = get_friends_query(user, page)
617
618 {:ok, Repo.all(q)}
619 end
620
621 def get_friends_ids(user, page \\ nil) do
622 q = get_friends_query(user, page)
623
624 Repo.all(from(u in q, select: u.id))
625 end
626
627 def get_follow_requests_query(%User{} = user) do
628 from(
629 a in Activity,
630 where:
631 fragment(
632 "? ->> 'type' = 'Follow'",
633 a.data
634 ),
635 where:
636 fragment(
637 "? ->> 'state' = 'pending'",
638 a.data
639 ),
640 where:
641 fragment(
642 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
643 a.data,
644 a.data,
645 ^user.ap_id
646 )
647 )
648 end
649
650 def get_follow_requests(%User{} = user) do
651 users =
652 user
653 |> User.get_follow_requests_query()
654 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
655 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
656 |> group_by([a, u], u.id)
657 |> select([a, u], u)
658 |> Repo.all()
659
660 {:ok, users}
661 end
662
663 def increase_note_count(%User{} = user) do
664 User
665 |> where(id: ^user.id)
666 |> update([u],
667 set: [
668 info:
669 fragment(
670 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
671 u.info,
672 u.info
673 )
674 ]
675 )
676 |> select([u], u)
677 |> Repo.update_all([])
678 |> case do
679 {1, [user]} -> set_cache(user)
680 _ -> {:error, user}
681 end
682 end
683
684 def decrease_note_count(%User{} = user) do
685 User
686 |> where(id: ^user.id)
687 |> update([u],
688 set: [
689 info:
690 fragment(
691 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
692 u.info,
693 u.info
694 )
695 ]
696 )
697 |> select([u], u)
698 |> Repo.update_all([])
699 |> case do
700 {1, [user]} -> set_cache(user)
701 _ -> {:error, user}
702 end
703 end
704
705 def update_note_count(%User{} = user) do
706 note_count_query =
707 from(
708 a in Object,
709 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
710 select: count(a.id)
711 )
712
713 note_count = Repo.one(note_count_query)
714
715 info_cng = User.Info.set_note_count(user.info, note_count)
716
717 cng =
718 change(user)
719 |> put_embed(:info, info_cng)
720
721 update_and_set_cache(cng)
722 end
723
724 def update_follower_count(%User{} = user) do
725 follower_count_query =
726 User
727 |> where([u], ^user.follower_address in u.following)
728 |> where([u], u.id != ^user.id)
729 |> select([u], %{count: count(u.id)})
730
731 User
732 |> where(id: ^user.id)
733 |> join(:inner, [u], s in subquery(follower_count_query))
734 |> update([u, s],
735 set: [
736 info:
737 fragment(
738 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
739 u.info,
740 s.count
741 )
742 ]
743 )
744 |> select([u], u)
745 |> Repo.update_all([])
746 |> case do
747 {1, [user]} -> set_cache(user)
748 _ -> {:error, user}
749 end
750 end
751
752 def get_users_from_set_query(ap_ids, false) do
753 from(
754 u in User,
755 where: u.ap_id in ^ap_ids
756 )
757 end
758
759 def get_users_from_set_query(ap_ids, true) do
760 query = get_users_from_set_query(ap_ids, false)
761
762 from(
763 u in query,
764 where: u.local == true
765 )
766 end
767
768 def get_users_from_set(ap_ids, local_only \\ true) do
769 get_users_from_set_query(ap_ids, local_only)
770 |> Repo.all()
771 end
772
773 def get_recipients_from_activity(%Activity{recipients: to}) do
774 query =
775 from(
776 u in User,
777 where: u.ap_id in ^to,
778 or_where: fragment("? && ?", u.following, ^to)
779 )
780
781 query = from(u in query, where: u.local == true)
782
783 Repo.all(query)
784 end
785
786 def search(query, resolve \\ false, for_user \\ nil) do
787 # Strip the beginning @ off if there is a query
788 query = String.trim_leading(query, "@")
789
790 if resolve, do: get_or_fetch(query)
791
792 {:ok, results} =
793 Repo.transaction(fn ->
794 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
795 Repo.all(search_query(query, for_user))
796 end)
797
798 results
799 end
800
801 def search_query(query, for_user) do
802 fts_subquery = fts_search_subquery(query)
803 trigram_subquery = trigram_search_subquery(query)
804 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
805 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
806
807 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
808 order_by: [desc: s.search_rank],
809 limit: 20
810 )
811 end
812
813 defp boost_search_rank_query(query, nil), do: query
814
815 defp boost_search_rank_query(query, for_user) do
816 friends_ids = get_friends_ids(for_user)
817 followers_ids = get_followers_ids(for_user)
818
819 from(u in subquery(query),
820 select_merge: %{
821 search_rank:
822 fragment(
823 """
824 CASE WHEN (?) THEN (?) * 1.3
825 WHEN (?) THEN (?) * 1.2
826 WHEN (?) THEN (?) * 1.1
827 ELSE (?) END
828 """,
829 u.id in ^friends_ids and u.id in ^followers_ids,
830 u.search_rank,
831 u.id in ^friends_ids,
832 u.search_rank,
833 u.id in ^followers_ids,
834 u.search_rank,
835 u.search_rank
836 )
837 }
838 )
839 end
840
841 defp fts_search_subquery(term, query \\ User) do
842 processed_query =
843 term
844 |> String.replace(~r/\W+/, " ")
845 |> String.trim()
846 |> String.split()
847 |> Enum.map(&(&1 <> ":*"))
848 |> Enum.join(" | ")
849
850 from(
851 u in query,
852 select_merge: %{
853 search_type: ^0,
854 search_rank:
855 fragment(
856 """
857 ts_rank_cd(
858 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
859 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
860 to_tsquery('simple', ?),
861 32
862 )
863 """,
864 u.nickname,
865 u.name,
866 ^processed_query
867 )
868 },
869 where:
870 fragment(
871 """
872 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
873 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
874 """,
875 u.nickname,
876 u.name,
877 ^processed_query
878 )
879 )
880 end
881
882 defp trigram_search_subquery(term) do
883 from(
884 u in User,
885 select_merge: %{
886 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
887 search_type: fragment("?", 1),
888 search_rank:
889 fragment(
890 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
891 ^term,
892 u.nickname,
893 u.name
894 )
895 },
896 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
897 )
898 end
899
900 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
901 Enum.map(
902 blocked_identifiers,
903 fn blocked_identifier ->
904 with %User{} = blocked <- get_or_fetch(blocked_identifier),
905 {:ok, blocker} <- block(blocker, blocked),
906 {:ok, _} <- ActivityPub.block(blocker, blocked) do
907 blocked
908 else
909 err ->
910 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
911 err
912 end
913 end
914 )
915 end
916
917 def mute(muter, %User{ap_id: ap_id}) do
918 info_cng =
919 muter.info
920 |> User.Info.add_to_mutes(ap_id)
921
922 cng =
923 change(muter)
924 |> put_embed(:info, info_cng)
925
926 update_and_set_cache(cng)
927 end
928
929 def unmute(muter, %{ap_id: ap_id}) do
930 info_cng =
931 muter.info
932 |> User.Info.remove_from_mutes(ap_id)
933
934 cng =
935 change(muter)
936 |> put_embed(:info, info_cng)
937
938 update_and_set_cache(cng)
939 end
940
941 def subscribe(subscriber, %{ap_id: ap_id}) do
942 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
943
944 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
945 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
946
947 if blocked do
948 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
949 else
950 info_cng =
951 subscribed.info
952 |> User.Info.add_to_subscribers(subscriber.ap_id)
953
954 change(subscribed)
955 |> put_embed(:info, info_cng)
956 |> update_and_set_cache()
957 end
958 end
959 end
960
961 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
962 with %User{} = user <- get_cached_by_ap_id(ap_id) do
963 info_cng =
964 user.info
965 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
966
967 change(user)
968 |> put_embed(:info, info_cng)
969 |> update_and_set_cache()
970 end
971 end
972
973 def block(blocker, %User{ap_id: ap_id} = blocked) do
974 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
975 blocker =
976 if following?(blocker, blocked) do
977 {:ok, blocker, _} = unfollow(blocker, blocked)
978 blocker
979 else
980 blocker
981 end
982
983 blocker =
984 if subscribed_to?(blocked, blocker) do
985 {:ok, blocker} = unsubscribe(blocked, blocker)
986 blocker
987 else
988 blocker
989 end
990
991 if following?(blocked, blocker) do
992 unfollow(blocked, blocker)
993 end
994
995 {:ok, blocker} = update_follower_count(blocker)
996
997 info_cng =
998 blocker.info
999 |> User.Info.add_to_block(ap_id)
1000
1001 cng =
1002 change(blocker)
1003 |> put_embed(:info, info_cng)
1004
1005 update_and_set_cache(cng)
1006 end
1007
1008 # helper to handle the block given only an actor's AP id
1009 def block(blocker, %{ap_id: ap_id}) do
1010 block(blocker, get_cached_by_ap_id(ap_id))
1011 end
1012
1013 def unblock(blocker, %{ap_id: ap_id}) do
1014 info_cng =
1015 blocker.info
1016 |> User.Info.remove_from_block(ap_id)
1017
1018 cng =
1019 change(blocker)
1020 |> put_embed(:info, info_cng)
1021
1022 update_and_set_cache(cng)
1023 end
1024
1025 def mutes?(nil, _), do: false
1026 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1027
1028 def blocks?(user, %{ap_id: ap_id}) do
1029 blocks = user.info.blocks
1030 domain_blocks = user.info.domain_blocks
1031 %{host: host} = URI.parse(ap_id)
1032
1033 Enum.member?(blocks, ap_id) ||
1034 Enum.any?(domain_blocks, fn domain ->
1035 host == domain
1036 end)
1037 end
1038
1039 def subscribed_to?(user, %{ap_id: ap_id}) do
1040 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1041 Enum.member?(target.info.subscribers, user.ap_id)
1042 end
1043 end
1044
1045 def muted_users(user),
1046 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1047
1048 def blocked_users(user),
1049 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1050
1051 def subscribers(user),
1052 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
1053
1054 def block_domain(user, domain) do
1055 info_cng =
1056 user.info
1057 |> User.Info.add_to_domain_block(domain)
1058
1059 cng =
1060 change(user)
1061 |> put_embed(:info, info_cng)
1062
1063 update_and_set_cache(cng)
1064 end
1065
1066 def unblock_domain(user, domain) do
1067 info_cng =
1068 user.info
1069 |> User.Info.remove_from_domain_block(domain)
1070
1071 cng =
1072 change(user)
1073 |> put_embed(:info, info_cng)
1074
1075 update_and_set_cache(cng)
1076 end
1077
1078 def maybe_local_user_query(query, local) do
1079 if local, do: local_user_query(query), else: query
1080 end
1081
1082 def local_user_query(query \\ User) do
1083 from(
1084 u in query,
1085 where: u.local == true,
1086 where: not is_nil(u.nickname)
1087 )
1088 end
1089
1090 def maybe_external_user_query(query, external) do
1091 if external, do: external_user_query(query), else: query
1092 end
1093
1094 def external_user_query(query \\ User) do
1095 from(
1096 u in query,
1097 where: u.local == false,
1098 where: not is_nil(u.nickname)
1099 )
1100 end
1101
1102 def maybe_active_user_query(query, active) do
1103 if active, do: active_user_query(query), else: query
1104 end
1105
1106 def active_user_query(query \\ User) do
1107 from(
1108 u in query,
1109 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1110 where: not is_nil(u.nickname)
1111 )
1112 end
1113
1114 def maybe_deactivated_user_query(query, deactivated) do
1115 if deactivated, do: deactivated_user_query(query), else: query
1116 end
1117
1118 def deactivated_user_query(query \\ User) do
1119 from(
1120 u in query,
1121 where: fragment("(?->'deactivated' @> 'true')", u.info),
1122 where: not is_nil(u.nickname)
1123 )
1124 end
1125
1126 def active_local_user_query do
1127 from(
1128 u in local_user_query(),
1129 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1130 )
1131 end
1132
1133 def moderator_user_query do
1134 from(
1135 u in User,
1136 where: u.local == true,
1137 where: fragment("?->'is_moderator' @> 'true'", u.info)
1138 )
1139 end
1140
1141 def deactivate(%User{} = user, status \\ true) do
1142 info_cng = User.Info.set_activation_status(user.info, status)
1143
1144 cng =
1145 change(user)
1146 |> put_embed(:info, info_cng)
1147
1148 update_and_set_cache(cng)
1149 end
1150
1151 def update_notification_settings(%User{} = user, settings \\ %{}) do
1152 info_changeset = User.Info.update_notification_settings(user.info, settings)
1153
1154 change(user)
1155 |> put_embed(:info, info_changeset)
1156 |> update_and_set_cache()
1157 end
1158
1159 def delete(%User{} = user) do
1160 {:ok, user} = User.deactivate(user)
1161
1162 # Remove all relationships
1163 {:ok, followers} = User.get_followers(user)
1164
1165 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1166
1167 {:ok, friends} = User.get_friends(user)
1168
1169 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1170
1171 delete_user_activities(user)
1172 end
1173
1174 def delete_user_activities(%User{ap_id: ap_id} = user) do
1175 Activity
1176 |> where(actor: ^ap_id)
1177 |> Activity.with_preloaded_object()
1178 |> Repo.all()
1179 |> Enum.each(fn
1180 %{data: %{"type" => "Create"}} = activity ->
1181 activity |> Object.normalize() |> ActivityPub.delete()
1182
1183 # TODO: Do something with likes, follows, repeats.
1184 _ ->
1185 "Doing nothing"
1186 end)
1187
1188 {:ok, user}
1189 end
1190
1191 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1192 Pleroma.HTML.Scrubber.TwitterText
1193 end
1194
1195 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1196
1197 def html_filter_policy(_), do: @default_scrubbers
1198
1199 def fetch_by_ap_id(ap_id) do
1200 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1201
1202 case ap_try do
1203 {:ok, user} ->
1204 user
1205
1206 _ ->
1207 case OStatus.make_user(ap_id) do
1208 {:ok, user} -> user
1209 _ -> {:error, "Could not fetch by AP id"}
1210 end
1211 end
1212 end
1213
1214 def get_or_fetch_by_ap_id(ap_id) do
1215 user = get_cached_by_ap_id(ap_id)
1216
1217 if !is_nil(user) and !User.needs_update?(user) do
1218 user
1219 else
1220 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1221 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1222
1223 user = fetch_by_ap_id(ap_id)
1224
1225 if should_fetch_initial do
1226 with %User{} = user do
1227 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1228 end
1229 end
1230
1231 user
1232 end
1233 end
1234
1235 def get_or_create_instance_user do
1236 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1237
1238 if user = get_cached_by_ap_id(relay_uri) do
1239 user
1240 else
1241 changes =
1242 %User{info: %User.Info{}}
1243 |> cast(%{}, [:ap_id, :nickname, :local])
1244 |> put_change(:ap_id, relay_uri)
1245 |> put_change(:nickname, nil)
1246 |> put_change(:local, true)
1247 |> put_change(:follower_address, relay_uri <> "/followers")
1248
1249 {:ok, user} = Repo.insert(changes)
1250 user
1251 end
1252 end
1253
1254 # AP style
1255 def public_key_from_info(%{
1256 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1257 }) do
1258 key =
1259 public_key_pem
1260 |> :public_key.pem_decode()
1261 |> hd()
1262 |> :public_key.pem_entry_decode()
1263
1264 {:ok, key}
1265 end
1266
1267 # OStatus Magic Key
1268 def public_key_from_info(%{magic_key: magic_key}) do
1269 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1270 end
1271
1272 def get_public_key_for_ap_id(ap_id) do
1273 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1274 {:ok, public_key} <- public_key_from_info(user.info) do
1275 {:ok, public_key}
1276 else
1277 _ -> :error
1278 end
1279 end
1280
1281 defp blank?(""), do: nil
1282 defp blank?(n), do: n
1283
1284 def insert_or_update_user(data) do
1285 data
1286 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1287 |> remote_user_creation()
1288 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1289 |> set_cache()
1290 end
1291
1292 def ap_enabled?(%User{local: true}), do: true
1293 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1294 def ap_enabled?(_), do: false
1295
1296 @doc "Gets or fetch a user by uri or nickname."
1297 @spec get_or_fetch(String.t()) :: User.t()
1298 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1299 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1300
1301 # wait a period of time and return newest version of the User structs
1302 # this is because we have synchronous follow APIs and need to simulate them
1303 # with an async handshake
1304 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1305 with %User{} = a <- User.get_cached_by_id(a.id),
1306 %User{} = b <- User.get_cached_by_id(b.id) do
1307 {:ok, a, b}
1308 else
1309 _e ->
1310 :error
1311 end
1312 end
1313
1314 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1315 with :ok <- :timer.sleep(timeout),
1316 %User{} = a <- User.get_cached_by_id(a.id),
1317 %User{} = b <- User.get_cached_by_id(b.id) do
1318 {:ok, a, b}
1319 else
1320 _e ->
1321 :error
1322 end
1323 end
1324
1325 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1326 def parse_bio(nil, _user), do: ""
1327 def parse_bio(bio, _user) when bio == "", do: bio
1328
1329 def parse_bio(bio, user) do
1330 emoji =
1331 (user.info.source_data["tag"] || [])
1332 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1333 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1334 {String.trim(name, ":"), url}
1335 end)
1336
1337 # TODO: get profile URLs other than user.ap_id
1338 profile_urls = [user.ap_id]
1339
1340 bio
1341 |> CommonUtils.format_input("text/plain",
1342 mentions_format: :full,
1343 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1344 )
1345 |> elem(0)
1346 |> Formatter.emojify(emoji)
1347 end
1348
1349 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1350 Repo.transaction(fn ->
1351 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1352 end)
1353 end
1354
1355 def tag(nickname, tags) when is_binary(nickname),
1356 do: tag(get_by_nickname(nickname), tags)
1357
1358 def tag(%User{} = user, tags),
1359 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1360
1361 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1362 Repo.transaction(fn ->
1363 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1364 end)
1365 end
1366
1367 def untag(nickname, tags) when is_binary(nickname),
1368 do: untag(get_by_nickname(nickname), tags)
1369
1370 def untag(%User{} = user, tags),
1371 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1372
1373 defp update_tags(%User{} = user, new_tags) do
1374 {:ok, updated_user} =
1375 user
1376 |> change(%{tags: new_tags})
1377 |> update_and_set_cache()
1378
1379 updated_user
1380 end
1381
1382 def bookmark(%User{} = user, status_id) do
1383 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1384 update_bookmarks(user, bookmarks)
1385 end
1386
1387 def unbookmark(%User{} = user, status_id) do
1388 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1389 update_bookmarks(user, bookmarks)
1390 end
1391
1392 def update_bookmarks(%User{} = user, bookmarks) do
1393 user
1394 |> change(%{bookmarks: bookmarks})
1395 |> update_and_set_cache
1396 end
1397
1398 defp normalize_tags(tags) do
1399 [tags]
1400 |> List.flatten()
1401 |> Enum.map(&String.downcase(&1))
1402 end
1403
1404 defp local_nickname_regex do
1405 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1406 @extended_local_nickname_regex
1407 else
1408 @strict_local_nickname_regex
1409 end
1410 end
1411
1412 def local_nickname(nickname_or_mention) do
1413 nickname_or_mention
1414 |> full_nickname()
1415 |> String.split("@")
1416 |> hd()
1417 end
1418
1419 def full_nickname(nickname_or_mention),
1420 do: String.trim_leading(nickname_or_mention, "@")
1421
1422 def error_user(ap_id) do
1423 %User{
1424 name: ap_id,
1425 ap_id: ap_id,
1426 info: %User.Info{},
1427 nickname: "erroruser@example.com",
1428 inserted_at: NaiveDateTime.utc_now()
1429 }
1430 end
1431
1432 def all_superusers do
1433 from(
1434 u in User,
1435 where: u.local == true,
1436 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1437 )
1438 |> Repo.all()
1439 end
1440
1441 defp paginate(query, page, page_size) do
1442 from(u in query,
1443 limit: ^page_size,
1444 offset: ^((page - 1) * page_size)
1445 )
1446 end
1447
1448 def showing_reblogs?(%User{} = user, %User{} = target) do
1449 target.ap_id not in user.info.muted_reblogs
1450 end
1451 end