Merge branch 'count-only-public-statuses' 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 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 q = get_follow_requests_query(user)
626 reqs = Repo.all(q)
627
628 users =
629 Enum.map(reqs, fn req -> req.actor end)
630 |> Enum.uniq()
631 |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
632 |> Enum.filter(fn u -> !is_nil(u) end)
633 |> Enum.filter(fn u -> !following?(u, user) end)
634
635 {:ok, users}
636 end
637
638 def increase_note_count(%User{} = user) do
639 info_cng = User.Info.add_to_note_count(user.info, 1)
640
641 cng =
642 change(user)
643 |> put_embed(:info, info_cng)
644
645 update_and_set_cache(cng)
646 end
647
648 def decrease_note_count(%User{} = user) do
649 info_cng = User.Info.add_to_note_count(user.info, -1)
650
651 cng =
652 change(user)
653 |> put_embed(:info, info_cng)
654
655 update_and_set_cache(cng)
656 end
657
658 def update_note_count(%User{} = user) do
659 note_count_query =
660 from(
661 a in Object,
662 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
663 select: count(a.id)
664 )
665
666 note_count = Repo.one(note_count_query)
667
668 info_cng = User.Info.set_note_count(user.info, note_count)
669
670 cng =
671 change(user)
672 |> put_embed(:info, info_cng)
673
674 update_and_set_cache(cng)
675 end
676
677 def update_follower_count(%User{} = user) do
678 follower_count_query =
679 from(
680 u in User,
681 where: ^user.follower_address in u.following,
682 where: u.id != ^user.id,
683 select: count(u.id)
684 )
685
686 follower_count = Repo.one(follower_count_query)
687
688 info_cng =
689 user.info
690 |> User.Info.set_follower_count(follower_count)
691
692 cng =
693 change(user)
694 |> put_embed(:info, info_cng)
695
696 update_and_set_cache(cng)
697 end
698
699 def get_users_from_set_query(ap_ids, false) do
700 from(
701 u in User,
702 where: u.ap_id in ^ap_ids
703 )
704 end
705
706 def get_users_from_set_query(ap_ids, true) do
707 query = get_users_from_set_query(ap_ids, false)
708
709 from(
710 u in query,
711 where: u.local == true
712 )
713 end
714
715 def get_users_from_set(ap_ids, local_only \\ true) do
716 get_users_from_set_query(ap_ids, local_only)
717 |> Repo.all()
718 end
719
720 def get_recipients_from_activity(%Activity{recipients: to}) do
721 query =
722 from(
723 u in User,
724 where: u.ap_id in ^to,
725 or_where: fragment("? && ?", u.following, ^to)
726 )
727
728 query = from(u in query, where: u.local == true)
729
730 Repo.all(query)
731 end
732
733 def search(query, resolve \\ false, for_user \\ nil) do
734 # Strip the beginning @ off if there is a query
735 query = String.trim_leading(query, "@")
736
737 if resolve, do: get_or_fetch(query)
738
739 fts_results = do_search(fts_search_subquery(query), for_user)
740
741 {:ok, trigram_results} =
742 Repo.transaction(fn ->
743 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
744 do_search(trigram_search_subquery(query), for_user)
745 end)
746
747 Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
748 end
749
750 def all_except_one(user) do
751 query = from(u in User, where: u.id != ^user.id)
752
753 Repo.all(query)
754 end
755
756 defp do_search(subquery, for_user, options \\ []) do
757 q =
758 from(
759 s in subquery(subquery),
760 order_by: [desc: s.search_rank],
761 limit: ^(options[:limit] || 20)
762 )
763
764 results =
765 q
766 |> Repo.all()
767 |> Enum.filter(&(&1.search_rank > 0))
768
769 boost_search_results(results, for_user)
770 end
771
772 defp fts_search_subquery(query) do
773 processed_query =
774 query
775 |> String.replace(~r/\W+/, " ")
776 |> String.trim()
777 |> String.split()
778 |> Enum.map(&(&1 <> ":*"))
779 |> Enum.join(" | ")
780
781 from(
782 u in User,
783 select_merge: %{
784 search_rank:
785 fragment(
786 """
787 ts_rank_cd(
788 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
789 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
790 to_tsquery('simple', ?),
791 32
792 )
793 """,
794 u.nickname,
795 u.name,
796 ^processed_query
797 )
798 },
799 where:
800 fragment(
801 """
802 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
803 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
804 """,
805 u.nickname,
806 u.name,
807 ^processed_query
808 )
809 )
810 end
811
812 defp trigram_search_subquery(query) do
813 from(
814 u in User,
815 select_merge: %{
816 search_rank:
817 fragment(
818 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
819 ^query,
820 u.nickname,
821 u.name
822 )
823 },
824 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
825 )
826 end
827
828 defp boost_search_results(results, nil), do: results
829
830 defp boost_search_results(results, for_user) do
831 friends_ids = get_friends_ids(for_user)
832 followers_ids = get_followers_ids(for_user)
833
834 Enum.map(
835 results,
836 fn u ->
837 search_rank_coef =
838 cond do
839 u.id in friends_ids ->
840 1.2
841
842 u.id in followers_ids ->
843 1.1
844
845 true ->
846 1
847 end
848
849 Map.put(u, :search_rank, u.search_rank * search_rank_coef)
850 end
851 )
852 |> Enum.sort_by(&(-&1.search_rank))
853 end
854
855 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
856 Enum.map(
857 blocked_identifiers,
858 fn blocked_identifier ->
859 with %User{} = blocked <- get_or_fetch(blocked_identifier),
860 {:ok, blocker} <- block(blocker, blocked),
861 {:ok, _} <- ActivityPub.block(blocker, blocked) do
862 blocked
863 else
864 err ->
865 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
866 err
867 end
868 end
869 )
870 end
871
872 def mute(muter, %User{ap_id: ap_id}) do
873 info_cng =
874 muter.info
875 |> User.Info.add_to_mutes(ap_id)
876
877 cng =
878 change(muter)
879 |> put_embed(:info, info_cng)
880
881 update_and_set_cache(cng)
882 end
883
884 def unmute(muter, %{ap_id: ap_id}) do
885 info_cng =
886 muter.info
887 |> User.Info.remove_from_mutes(ap_id)
888
889 cng =
890 change(muter)
891 |> put_embed(:info, info_cng)
892
893 update_and_set_cache(cng)
894 end
895
896 def block(blocker, %User{ap_id: ap_id} = blocked) do
897 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
898 blocker =
899 if following?(blocker, blocked) do
900 {:ok, blocker, _} = unfollow(blocker, blocked)
901 blocker
902 else
903 blocker
904 end
905
906 if following?(blocked, blocker) do
907 unfollow(blocked, blocker)
908 end
909
910 info_cng =
911 blocker.info
912 |> User.Info.add_to_block(ap_id)
913
914 cng =
915 change(blocker)
916 |> put_embed(:info, info_cng)
917
918 update_and_set_cache(cng)
919 end
920
921 # helper to handle the block given only an actor's AP id
922 def block(blocker, %{ap_id: ap_id}) do
923 block(blocker, User.get_by_ap_id(ap_id))
924 end
925
926 def unblock(blocker, %{ap_id: ap_id}) do
927 info_cng =
928 blocker.info
929 |> User.Info.remove_from_block(ap_id)
930
931 cng =
932 change(blocker)
933 |> put_embed(:info, info_cng)
934
935 update_and_set_cache(cng)
936 end
937
938 def mutes?(nil, _), do: false
939 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
940
941 def blocks?(user, %{ap_id: ap_id}) do
942 blocks = user.info.blocks
943 domain_blocks = user.info.domain_blocks
944 %{host: host} = URI.parse(ap_id)
945
946 Enum.member?(blocks, ap_id) ||
947 Enum.any?(domain_blocks, fn domain ->
948 host == domain
949 end)
950 end
951
952 def muted_users(user),
953 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
954
955 def blocked_users(user),
956 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
957
958 def block_domain(user, domain) do
959 info_cng =
960 user.info
961 |> User.Info.add_to_domain_block(domain)
962
963 cng =
964 change(user)
965 |> put_embed(:info, info_cng)
966
967 update_and_set_cache(cng)
968 end
969
970 def unblock_domain(user, domain) do
971 info_cng =
972 user.info
973 |> User.Info.remove_from_domain_block(domain)
974
975 cng =
976 change(user)
977 |> put_embed(:info, info_cng)
978
979 update_and_set_cache(cng)
980 end
981
982 def local_user_query do
983 from(
984 u in User,
985 where: u.local == true,
986 where: not is_nil(u.nickname)
987 )
988 end
989
990 def active_local_user_query do
991 from(
992 u in local_user_query(),
993 where: fragment("not (?->'deactivated' @> 'true')", u.info)
994 )
995 end
996
997 def moderator_user_query do
998 from(
999 u in User,
1000 where: u.local == true,
1001 where: fragment("?->'is_moderator' @> 'true'", u.info)
1002 )
1003 end
1004
1005 def deactivate(%User{} = user, status \\ true) do
1006 info_cng = User.Info.set_activation_status(user.info, status)
1007
1008 cng =
1009 change(user)
1010 |> put_embed(:info, info_cng)
1011
1012 update_and_set_cache(cng)
1013 end
1014
1015 def delete(%User{} = user) do
1016 {:ok, user} = User.deactivate(user)
1017
1018 # Remove all relationships
1019 {:ok, followers} = User.get_followers(user)
1020
1021 followers
1022 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
1023
1024 {:ok, friends} = User.get_friends(user)
1025
1026 friends
1027 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
1028
1029 query = from(a in Activity, where: a.actor == ^user.ap_id)
1030
1031 Repo.all(query)
1032 |> Enum.each(fn activity ->
1033 case activity.data["type"] do
1034 "Create" ->
1035 ActivityPub.delete(Object.normalize(activity.data["object"]))
1036
1037 # TODO: Do something with likes, follows, repeats.
1038 _ ->
1039 "Doing nothing"
1040 end
1041 end)
1042
1043 {:ok, user}
1044 end
1045
1046 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1047 Pleroma.HTML.Scrubber.TwitterText
1048 end
1049
1050 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1051
1052 def html_filter_policy(_), do: @default_scrubbers
1053
1054 def get_or_fetch_by_ap_id(ap_id) do
1055 user = get_by_ap_id(ap_id)
1056
1057 if !is_nil(user) and !User.needs_update?(user) do
1058 user
1059 else
1060 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1061
1062 case ap_try do
1063 {:ok, user} ->
1064 user
1065
1066 _ ->
1067 case OStatus.make_user(ap_id) do
1068 {:ok, user} -> user
1069 _ -> {:error, "Could not fetch by AP id"}
1070 end
1071 end
1072 end
1073 end
1074
1075 def get_or_create_instance_user do
1076 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1077
1078 if user = get_by_ap_id(relay_uri) do
1079 user
1080 else
1081 changes =
1082 %User{info: %User.Info{}}
1083 |> cast(%{}, [:ap_id, :nickname, :local])
1084 |> put_change(:ap_id, relay_uri)
1085 |> put_change(:nickname, nil)
1086 |> put_change(:local, true)
1087 |> put_change(:follower_address, relay_uri <> "/followers")
1088
1089 {:ok, user} = Repo.insert(changes)
1090 user
1091 end
1092 end
1093
1094 # AP style
1095 def public_key_from_info(%{
1096 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1097 }) do
1098 key =
1099 public_key_pem
1100 |> :public_key.pem_decode()
1101 |> hd()
1102 |> :public_key.pem_entry_decode()
1103
1104 {:ok, key}
1105 end
1106
1107 # OStatus Magic Key
1108 def public_key_from_info(%{magic_key: magic_key}) do
1109 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1110 end
1111
1112 def get_public_key_for_ap_id(ap_id) do
1113 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1114 {:ok, public_key} <- public_key_from_info(user.info) do
1115 {:ok, public_key}
1116 else
1117 _ -> :error
1118 end
1119 end
1120
1121 defp blank?(""), do: nil
1122 defp blank?(n), do: n
1123
1124 def insert_or_update_user(data) do
1125 data =
1126 data
1127 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1128
1129 cs = User.remote_user_creation(data)
1130
1131 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1132 end
1133
1134 def ap_enabled?(%User{local: true}), do: true
1135 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1136 def ap_enabled?(_), do: false
1137
1138 @doc "Gets or fetch a user by uri or nickname."
1139 @spec get_or_fetch(String.t()) :: User.t()
1140 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1141 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1142
1143 # wait a period of time and return newest version of the User structs
1144 # this is because we have synchronous follow APIs and need to simulate them
1145 # with an async handshake
1146 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1147 with %User{} = a <- Repo.get(User, a.id),
1148 %User{} = b <- Repo.get(User, b.id) do
1149 {:ok, a, b}
1150 else
1151 _e ->
1152 :error
1153 end
1154 end
1155
1156 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1157 with :ok <- :timer.sleep(timeout),
1158 %User{} = a <- Repo.get(User, a.id),
1159 %User{} = b <- Repo.get(User, b.id) do
1160 {:ok, a, b}
1161 else
1162 _e ->
1163 :error
1164 end
1165 end
1166
1167 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1168 def parse_bio(nil, _user), do: ""
1169 def parse_bio(bio, _user) when bio == "", do: bio
1170
1171 def parse_bio(bio, user) do
1172 emoji =
1173 (user.info.source_data["tag"] || [])
1174 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1175 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1176 {String.trim(name, ":"), url}
1177 end)
1178
1179 bio
1180 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1181 |> elem(0)
1182 |> Formatter.emojify(emoji)
1183 end
1184
1185 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1186 Repo.transaction(fn ->
1187 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1188 end)
1189 end
1190
1191 def tag(nickname, tags) when is_binary(nickname),
1192 do: tag(User.get_by_nickname(nickname), tags)
1193
1194 def tag(%User{} = user, tags),
1195 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1196
1197 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1198 Repo.transaction(fn ->
1199 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1200 end)
1201 end
1202
1203 def untag(nickname, tags) when is_binary(nickname),
1204 do: untag(User.get_by_nickname(nickname), tags)
1205
1206 def untag(%User{} = user, tags),
1207 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1208
1209 defp update_tags(%User{} = user, new_tags) do
1210 {:ok, updated_user} =
1211 user
1212 |> change(%{tags: new_tags})
1213 |> update_and_set_cache()
1214
1215 updated_user
1216 end
1217
1218 def bookmark(%User{} = user, status_id) do
1219 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1220 update_bookmarks(user, bookmarks)
1221 end
1222
1223 def unbookmark(%User{} = user, status_id) do
1224 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1225 update_bookmarks(user, bookmarks)
1226 end
1227
1228 def update_bookmarks(%User{} = user, bookmarks) do
1229 user
1230 |> change(%{bookmarks: bookmarks})
1231 |> update_and_set_cache
1232 end
1233
1234 defp normalize_tags(tags) do
1235 [tags]
1236 |> List.flatten()
1237 |> Enum.map(&String.downcase(&1))
1238 end
1239
1240 defp local_nickname_regex() do
1241 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1242 @extended_local_nickname_regex
1243 else
1244 @strict_local_nickname_regex
1245 end
1246 end
1247
1248 def local_nickname(nickname_or_mention) do
1249 nickname_or_mention
1250 |> full_nickname()
1251 |> String.split("@")
1252 |> hd()
1253 end
1254
1255 def full_nickname(nickname_or_mention),
1256 do: String.trim_leading(nickname_or_mention, "@")
1257
1258 def error_user(ap_id) do
1259 %User{
1260 name: ap_id,
1261 ap_id: ap_id,
1262 info: %User.Info{},
1263 nickname: "erroruser@example.com",
1264 inserted_at: NaiveDateTime.utc_now()
1265 }
1266 end
1267
1268 def all_superusers do
1269 from(
1270 u in User,
1271 where: u.local == true,
1272 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1273 )
1274 |> Repo.all()
1275 end
1276 end