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