mark ap_id unique_constraint
[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, _} <- try_send_confirmation_email(user) do
266 {:ok, user}
267 end
268 end
269
270 def try_send_confirmation_email(%User{} = user) do
271 if user.info.confirmation_pending &&
272 Pleroma.Config.get([:instance, :account_activation_required]) do
273 user
274 |> Pleroma.UserEmail.account_confirmation_email()
275 |> Pleroma.Mailer.deliver()
276 else
277 {:ok, :noop}
278 end
279 end
280
281 def needs_update?(%User{local: true}), do: false
282
283 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
284
285 def needs_update?(%User{local: false} = user) do
286 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400
287 end
288
289 def needs_update?(_), do: true
290
291 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
292 {:ok, follower}
293 end
294
295 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
296 follow(follower, followed)
297 end
298
299 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
300 if not User.ap_enabled?(followed) do
301 follow(follower, followed)
302 else
303 {:ok, follower}
304 end
305 end
306
307 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
308 if not following?(follower, followed) do
309 follow(follower, followed)
310 else
311 {:ok, follower}
312 end
313 end
314
315 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
316 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
317 def follow_all(follower, followeds) do
318 followed_addresses =
319 followeds
320 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
321 |> Enum.map(fn %{follower_address: fa} -> fa end)
322
323 q =
324 from(u in User,
325 where: u.id == ^follower.id,
326 update: [
327 set: [
328 following:
329 fragment(
330 "array(select distinct unnest (array_cat(?, ?)))",
331 u.following,
332 ^followed_addresses
333 )
334 ]
335 ]
336 )
337
338 {1, [follower]} = Repo.update_all(q, [], returning: true)
339
340 Enum.each(followeds, fn followed ->
341 update_follower_count(followed)
342 end)
343
344 set_cache(follower)
345 end
346
347 def follow(%User{} = follower, %User{info: info} = followed) do
348 user_config = Application.get_env(:pleroma, :user)
349 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
350
351 ap_followers = followed.follower_address
352
353 cond do
354 following?(follower, followed) or info.deactivated ->
355 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
356
357 deny_follow_blocked and blocks?(followed, follower) ->
358 {:error, "Could not follow user: #{followed.nickname} blocked you."}
359
360 true ->
361 if !followed.local && follower.local && !ap_enabled?(followed) do
362 Websub.subscribe(follower, followed)
363 end
364
365 q =
366 from(u in User,
367 where: u.id == ^follower.id,
368 update: [push: [following: ^ap_followers]]
369 )
370
371 {1, [follower]} = Repo.update_all(q, [], returning: true)
372
373 {:ok, _} = update_follower_count(followed)
374
375 set_cache(follower)
376 end
377 end
378
379 def unfollow(%User{} = follower, %User{} = followed) do
380 ap_followers = followed.follower_address
381
382 if following?(follower, followed) and follower.ap_id != followed.ap_id do
383 q =
384 from(u in User,
385 where: u.id == ^follower.id,
386 update: [pull: [following: ^ap_followers]]
387 )
388
389 {1, [follower]} = Repo.update_all(q, [], returning: true)
390
391 {:ok, followed} = update_follower_count(followed)
392
393 set_cache(follower)
394
395 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
396 else
397 {:error, "Not subscribed!"}
398 end
399 end
400
401 @spec following?(User.t(), User.t()) :: boolean
402 def following?(%User{} = follower, %User{} = followed) do
403 Enum.member?(follower.following, followed.follower_address)
404 end
405
406 def follow_import(%User{} = follower, followed_identifiers)
407 when is_list(followed_identifiers) do
408 Enum.map(
409 followed_identifiers,
410 fn followed_identifier ->
411 with %User{} = followed <- get_or_fetch(followed_identifier),
412 {:ok, follower} <- maybe_direct_follow(follower, followed),
413 {:ok, _} <- ActivityPub.follow(follower, followed) do
414 followed
415 else
416 err ->
417 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
418 err
419 end
420 end
421 )
422 end
423
424 def locked?(%User{} = user) do
425 user.info.locked || false
426 end
427
428 def get_by_id(id) do
429 Repo.get_by(User, id: id)
430 end
431
432 def get_by_ap_id(ap_id) do
433 Repo.get_by(User, ap_id: ap_id)
434 end
435
436 # 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
437 def get_by_guessed_nickname(ap_id) do
438 domain = URI.parse(ap_id).host
439 name = List.last(String.split(ap_id, "/"))
440 nickname = "#{name}@#{domain}"
441
442 get_by_nickname(nickname)
443 end
444
445 def set_cache(user) do
446 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
447 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
448 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
449 {:ok, user}
450 end
451
452 def update_and_set_cache(changeset) do
453 with {:ok, user} <- Repo.update(changeset) do
454 set_cache(user)
455 else
456 e -> e
457 end
458 end
459
460 def invalidate_cache(user) do
461 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
462 Cachex.del(:user_cache, "nickname:#{user.nickname}")
463 Cachex.del(:user_cache, "user_info:#{user.id}")
464 end
465
466 def get_cached_by_ap_id(ap_id) do
467 key = "ap_id:#{ap_id}"
468 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
469 end
470
471 def get_cached_by_id(id) do
472 key = "id:#{id}"
473
474 ap_id =
475 Cachex.fetch!(:user_cache, key, fn _ ->
476 user = get_by_id(id)
477
478 if user do
479 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
480 {:commit, user.ap_id}
481 else
482 {:ignore, ""}
483 end
484 end)
485
486 get_cached_by_ap_id(ap_id)
487 end
488
489 def get_cached_by_nickname(nickname) do
490 key = "nickname:#{nickname}"
491 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
492 end
493
494 def get_cached_by_nickname_or_id(nickname_or_id) do
495 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
496 end
497
498 def get_by_nickname(nickname) do
499 Repo.get_by(User, nickname: nickname) ||
500 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
501 Repo.get_by(User, nickname: local_nickname(nickname))
502 end
503 end
504
505 def get_by_nickname_or_email(nickname_or_email) do
506 case user = Repo.get_by(User, nickname: nickname_or_email) do
507 %User{} -> user
508 nil -> Repo.get_by(User, email: nickname_or_email)
509 end
510 end
511
512 def get_cached_user_info(user) do
513 key = "user_info:#{user.id}"
514 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
515 end
516
517 def fetch_by_nickname(nickname) do
518 ap_try = ActivityPub.make_user_from_nickname(nickname)
519
520 case ap_try do
521 {:ok, user} -> {:ok, user}
522 _ -> OStatus.make_user(nickname)
523 end
524 end
525
526 def get_or_fetch_by_nickname(nickname) do
527 with %User{} = user <- get_by_nickname(nickname) do
528 user
529 else
530 _e ->
531 with [_nick, _domain] <- String.split(nickname, "@"),
532 {:ok, user} <- fetch_by_nickname(nickname) do
533 user
534 else
535 _e -> nil
536 end
537 end
538 end
539
540 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
541 from(
542 u in User,
543 where: fragment("? <@ ?", ^[follower_address], u.following),
544 where: u.id != ^id
545 )
546 end
547
548 def get_followers_query(user, page) do
549 from(
550 u in get_followers_query(user, nil),
551 limit: 20,
552 offset: ^((page - 1) * 20)
553 )
554 end
555
556 def get_followers_query(user), do: get_followers_query(user, nil)
557
558 def get_followers(user, page \\ nil) do
559 q = get_followers_query(user, page)
560
561 {:ok, Repo.all(q)}
562 end
563
564 def get_followers_ids(user, page \\ nil) do
565 q = get_followers_query(user, page)
566
567 Repo.all(from(u in q, select: u.id))
568 end
569
570 def get_friends_query(%User{id: id, following: following}, nil) do
571 from(
572 u in User,
573 where: u.follower_address in ^following,
574 where: u.id != ^id
575 )
576 end
577
578 def get_friends_query(user, page) do
579 from(
580 u in get_friends_query(user, nil),
581 limit: 20,
582 offset: ^((page - 1) * 20)
583 )
584 end
585
586 def get_friends_query(user), do: get_friends_query(user, nil)
587
588 def get_friends(user, page \\ nil) do
589 q = get_friends_query(user, page)
590
591 {:ok, Repo.all(q)}
592 end
593
594 def get_friends_ids(user, page \\ nil) do
595 q = get_friends_query(user, page)
596
597 Repo.all(from(u in q, select: u.id))
598 end
599
600 def get_follow_requests_query(%User{} = user) do
601 from(
602 a in Activity,
603 where:
604 fragment(
605 "? ->> 'type' = 'Follow'",
606 a.data
607 ),
608 where:
609 fragment(
610 "? ->> 'state' = 'pending'",
611 a.data
612 ),
613 where:
614 fragment(
615 "? @> ?",
616 a.data,
617 ^%{"object" => user.ap_id}
618 )
619 )
620 end
621
622 def get_follow_requests(%User{} = user) do
623 q = get_follow_requests_query(user)
624 reqs = Repo.all(q)
625
626 users =
627 Enum.map(reqs, fn req -> req.actor end)
628 |> Enum.uniq()
629 |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
630 |> Enum.filter(fn u -> !is_nil(u) end)
631 |> Enum.filter(fn u -> !following?(u, user) end)
632
633 {:ok, users}
634 end
635
636 def increase_note_count(%User{} = user) do
637 info_cng = User.Info.add_to_note_count(user.info, 1)
638
639 cng =
640 change(user)
641 |> put_embed(:info, info_cng)
642
643 update_and_set_cache(cng)
644 end
645
646 def decrease_note_count(%User{} = user) do
647 info_cng = User.Info.add_to_note_count(user.info, -1)
648
649 cng =
650 change(user)
651 |> put_embed(:info, info_cng)
652
653 update_and_set_cache(cng)
654 end
655
656 def update_note_count(%User{} = user) do
657 note_count_query =
658 from(
659 a in Object,
660 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
661 select: count(a.id)
662 )
663
664 note_count = Repo.one(note_count_query)
665
666 info_cng = User.Info.set_note_count(user.info, note_count)
667
668 cng =
669 change(user)
670 |> put_embed(:info, info_cng)
671
672 update_and_set_cache(cng)
673 end
674
675 def update_follower_count(%User{} = user) do
676 follower_count_query =
677 from(
678 u in User,
679 where: ^user.follower_address in u.following,
680 where: u.id != ^user.id,
681 select: count(u.id)
682 )
683
684 follower_count = Repo.one(follower_count_query)
685
686 info_cng =
687 user.info
688 |> User.Info.set_follower_count(follower_count)
689
690 cng =
691 change(user)
692 |> put_embed(:info, info_cng)
693
694 update_and_set_cache(cng)
695 end
696
697 def get_users_from_set_query(ap_ids, false) do
698 from(
699 u in User,
700 where: u.ap_id in ^ap_ids
701 )
702 end
703
704 def get_users_from_set_query(ap_ids, true) do
705 query = get_users_from_set_query(ap_ids, false)
706
707 from(
708 u in query,
709 where: u.local == true
710 )
711 end
712
713 def get_users_from_set(ap_ids, local_only \\ true) do
714 get_users_from_set_query(ap_ids, local_only)
715 |> Repo.all()
716 end
717
718 def get_recipients_from_activity(%Activity{recipients: to}) do
719 query =
720 from(
721 u in User,
722 where: u.ap_id in ^to,
723 or_where: fragment("? && ?", u.following, ^to)
724 )
725
726 query = from(u in query, where: u.local == true)
727
728 Repo.all(query)
729 end
730
731 def search(query, resolve \\ false, for_user \\ nil) do
732 # Strip the beginning @ off if there is a query
733 query = String.trim_leading(query, "@")
734
735 if resolve, do: get_or_fetch(query)
736
737 fts_results = do_search(fts_search_subquery(query), for_user)
738
739 {:ok, trigram_results} =
740 Repo.transaction(fn ->
741 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
742 do_search(trigram_search_subquery(query), for_user)
743 end)
744
745 Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
746 end
747
748 defp do_search(subquery, for_user, options \\ []) do
749 q =
750 from(
751 s in subquery(subquery),
752 order_by: [desc: s.search_rank],
753 limit: ^(options[:limit] || 20)
754 )
755
756 results =
757 q
758 |> Repo.all()
759 |> Enum.filter(&(&1.search_rank > 0))
760
761 boost_search_results(results, for_user)
762 end
763
764 defp fts_search_subquery(query) do
765 processed_query =
766 query
767 |> String.replace(~r/\W+/, " ")
768 |> String.trim()
769 |> String.split()
770 |> Enum.map(&(&1 <> ":*"))
771 |> Enum.join(" | ")
772
773 from(
774 u in User,
775 select_merge: %{
776 search_rank:
777 fragment(
778 """
779 ts_rank_cd(
780 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
781 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
782 to_tsquery('simple', ?),
783 32
784 )
785 """,
786 u.nickname,
787 u.name,
788 ^processed_query
789 )
790 },
791 where:
792 fragment(
793 """
794 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
795 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
796 """,
797 u.nickname,
798 u.name,
799 ^processed_query
800 )
801 )
802 end
803
804 defp trigram_search_subquery(query) do
805 from(
806 u in User,
807 select_merge: %{
808 search_rank:
809 fragment(
810 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
811 ^query,
812 u.nickname,
813 u.name
814 )
815 },
816 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
817 )
818 end
819
820 defp boost_search_results(results, nil), do: results
821
822 defp boost_search_results(results, for_user) do
823 friends_ids = get_friends_ids(for_user)
824 followers_ids = get_followers_ids(for_user)
825
826 Enum.map(
827 results,
828 fn u ->
829 search_rank_coef =
830 cond do
831 u.id in friends_ids ->
832 1.2
833
834 u.id in followers_ids ->
835 1.1
836
837 true ->
838 1
839 end
840
841 Map.put(u, :search_rank, u.search_rank * search_rank_coef)
842 end
843 )
844 |> Enum.sort_by(&(-&1.search_rank))
845 end
846
847 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
848 Enum.map(
849 blocked_identifiers,
850 fn blocked_identifier ->
851 with %User{} = blocked <- get_or_fetch(blocked_identifier),
852 {:ok, blocker} <- block(blocker, blocked),
853 {:ok, _} <- ActivityPub.block(blocker, blocked) do
854 blocked
855 else
856 err ->
857 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
858 err
859 end
860 end
861 )
862 end
863
864 def block(blocker, %User{ap_id: ap_id} = blocked) do
865 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
866 blocker =
867 if following?(blocker, blocked) do
868 {:ok, blocker, _} = unfollow(blocker, blocked)
869 blocker
870 else
871 blocker
872 end
873
874 if following?(blocked, blocker) do
875 unfollow(blocked, blocker)
876 end
877
878 info_cng =
879 blocker.info
880 |> User.Info.add_to_block(ap_id)
881
882 cng =
883 change(blocker)
884 |> put_embed(:info, info_cng)
885
886 update_and_set_cache(cng)
887 end
888
889 # helper to handle the block given only an actor's AP id
890 def block(blocker, %{ap_id: ap_id}) do
891 block(blocker, User.get_by_ap_id(ap_id))
892 end
893
894 def unblock(blocker, %{ap_id: ap_id}) do
895 info_cng =
896 blocker.info
897 |> User.Info.remove_from_block(ap_id)
898
899 cng =
900 change(blocker)
901 |> put_embed(:info, info_cng)
902
903 update_and_set_cache(cng)
904 end
905
906 def blocks?(user, %{ap_id: ap_id}) do
907 blocks = user.info.blocks
908 domain_blocks = user.info.domain_blocks
909 %{host: host} = URI.parse(ap_id)
910
911 Enum.member?(blocks, ap_id) ||
912 Enum.any?(domain_blocks, fn domain ->
913 host == domain
914 end)
915 end
916
917 def blocked_users(user),
918 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
919
920 def block_domain(user, domain) do
921 info_cng =
922 user.info
923 |> User.Info.add_to_domain_block(domain)
924
925 cng =
926 change(user)
927 |> put_embed(:info, info_cng)
928
929 update_and_set_cache(cng)
930 end
931
932 def unblock_domain(user, domain) do
933 info_cng =
934 user.info
935 |> User.Info.remove_from_domain_block(domain)
936
937 cng =
938 change(user)
939 |> put_embed(:info, info_cng)
940
941 update_and_set_cache(cng)
942 end
943
944 def local_user_query do
945 from(
946 u in User,
947 where: u.local == true,
948 where: not is_nil(u.nickname)
949 )
950 end
951
952 def active_local_user_query do
953 from(
954 u in local_user_query(),
955 where: fragment("not (?->'deactivated' @> 'true')", u.info)
956 )
957 end
958
959 def moderator_user_query do
960 from(
961 u in User,
962 where: u.local == true,
963 where: fragment("?->'is_moderator' @> 'true'", u.info)
964 )
965 end
966
967 def deactivate(%User{} = user, status \\ true) do
968 info_cng = User.Info.set_activation_status(user.info, status)
969
970 cng =
971 change(user)
972 |> put_embed(:info, info_cng)
973
974 update_and_set_cache(cng)
975 end
976
977 def delete(%User{} = user) do
978 {:ok, user} = User.deactivate(user)
979
980 # Remove all relationships
981 {:ok, followers} = User.get_followers(user)
982
983 followers
984 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
985
986 {:ok, friends} = User.get_friends(user)
987
988 friends
989 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
990
991 query = from(a in Activity, where: a.actor == ^user.ap_id)
992
993 Repo.all(query)
994 |> Enum.each(fn activity ->
995 case activity.data["type"] do
996 "Create" ->
997 ActivityPub.delete(Object.normalize(activity.data["object"]))
998
999 # TODO: Do something with likes, follows, repeats.
1000 _ ->
1001 "Doing nothing"
1002 end
1003 end)
1004
1005 {:ok, user}
1006 end
1007
1008 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1009 Pleroma.HTML.Scrubber.TwitterText
1010 end
1011
1012 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1013
1014 def html_filter_policy(_), do: @default_scrubbers
1015
1016 def get_or_fetch_by_ap_id(ap_id) do
1017 user = get_by_ap_id(ap_id)
1018
1019 if !is_nil(user) and !User.needs_update?(user) do
1020 user
1021 else
1022 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1023
1024 case ap_try do
1025 {:ok, user} ->
1026 user
1027
1028 _ ->
1029 case OStatus.make_user(ap_id) do
1030 {:ok, user} -> user
1031 _ -> {:error, "Could not fetch by AP id"}
1032 end
1033 end
1034 end
1035 end
1036
1037 def get_or_create_instance_user do
1038 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1039
1040 if user = get_by_ap_id(relay_uri) do
1041 user
1042 else
1043 changes =
1044 %User{info: %User.Info{}}
1045 |> cast(%{}, [:ap_id, :nickname, :local])
1046 |> put_change(:ap_id, relay_uri)
1047 |> put_change(:nickname, nil)
1048 |> put_change(:local, true)
1049 |> put_change(:follower_address, relay_uri <> "/followers")
1050
1051 {:ok, user} = Repo.insert(changes)
1052 user
1053 end
1054 end
1055
1056 # AP style
1057 def public_key_from_info(%{
1058 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1059 }) do
1060 key =
1061 public_key_pem
1062 |> :public_key.pem_decode()
1063 |> hd()
1064 |> :public_key.pem_entry_decode()
1065
1066 {:ok, key}
1067 end
1068
1069 # OStatus Magic Key
1070 def public_key_from_info(%{magic_key: magic_key}) do
1071 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1072 end
1073
1074 def get_public_key_for_ap_id(ap_id) do
1075 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1076 {:ok, public_key} <- public_key_from_info(user.info) do
1077 {:ok, public_key}
1078 else
1079 _ -> :error
1080 end
1081 end
1082
1083 defp blank?(""), do: nil
1084 defp blank?(n), do: n
1085
1086 def insert_or_update_user(data) do
1087 data =
1088 data
1089 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1090
1091 cs = User.remote_user_creation(data)
1092
1093 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1094 end
1095
1096 def ap_enabled?(%User{local: true}), do: true
1097 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1098 def ap_enabled?(_), do: false
1099
1100 @doc "Gets or fetch a user by uri or nickname."
1101 @spec get_or_fetch(String.t()) :: User.t()
1102 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1103 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1104
1105 # wait a period of time and return newest version of the User structs
1106 # this is because we have synchronous follow APIs and need to simulate them
1107 # with an async handshake
1108 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1109 with %User{} = a <- Repo.get(User, a.id),
1110 %User{} = b <- Repo.get(User, b.id) do
1111 {:ok, a, b}
1112 else
1113 _e ->
1114 :error
1115 end
1116 end
1117
1118 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1119 with :ok <- :timer.sleep(timeout),
1120 %User{} = a <- Repo.get(User, a.id),
1121 %User{} = b <- Repo.get(User, b.id) do
1122 {:ok, a, b}
1123 else
1124 _e ->
1125 :error
1126 end
1127 end
1128
1129 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1130 def parse_bio(nil, _user), do: ""
1131 def parse_bio(bio, _user) when bio == "", do: bio
1132
1133 def parse_bio(bio, user) do
1134 mentions = Formatter.parse_mentions(bio)
1135 tags = Formatter.parse_tags(bio)
1136
1137 emoji =
1138 (user.info.source_data["tag"] || [])
1139 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1140 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1141 {String.trim(name, ":"), url}
1142 end)
1143
1144 bio
1145 |> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full])
1146 |> Formatter.emojify(emoji)
1147 end
1148
1149 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1150 Repo.transaction(fn ->
1151 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1152 end)
1153 end
1154
1155 def tag(nickname, tags) when is_binary(nickname),
1156 do: tag(User.get_by_nickname(nickname), tags)
1157
1158 def tag(%User{} = user, tags),
1159 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1160
1161 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1162 Repo.transaction(fn ->
1163 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1164 end)
1165 end
1166
1167 def untag(nickname, tags) when is_binary(nickname),
1168 do: untag(User.get_by_nickname(nickname), tags)
1169
1170 def untag(%User{} = user, tags),
1171 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1172
1173 defp update_tags(%User{} = user, new_tags) do
1174 {:ok, updated_user} =
1175 user
1176 |> change(%{tags: new_tags})
1177 |> Repo.update()
1178
1179 updated_user
1180 end
1181
1182 def bookmark(%User{} = user, status_id) do
1183 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1184 update_bookmarks(user, bookmarks)
1185 end
1186
1187 def unbookmark(%User{} = user, status_id) do
1188 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1189 update_bookmarks(user, bookmarks)
1190 end
1191
1192 def update_bookmarks(%User{} = user, bookmarks) do
1193 user
1194 |> change(%{bookmarks: bookmarks})
1195 |> update_and_set_cache
1196 end
1197
1198 defp normalize_tags(tags) do
1199 [tags]
1200 |> List.flatten()
1201 |> Enum.map(&String.downcase(&1))
1202 end
1203
1204 defp local_nickname_regex() do
1205 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1206 @extended_local_nickname_regex
1207 else
1208 @strict_local_nickname_regex
1209 end
1210 end
1211
1212 def local_nickname(nickname_or_mention) do
1213 nickname_or_mention
1214 |> full_nickname()
1215 |> String.split("@")
1216 |> hd()
1217 end
1218
1219 def full_nickname(nickname_or_mention),
1220 do: String.trim_leading(nickname_or_mention, "@")
1221
1222 def error_user(ap_id) do
1223 %User{
1224 name: ap_id,
1225 ap_id: ap_id,
1226 info: %User.Info{},
1227 nickname: "erroruser@example.com",
1228 inserted_at: NaiveDateTime.utc_now()
1229 }
1230 end
1231 end