merge develop
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Comeonin.Pbkdf2
12 alias Ecto.Multi
13 alias Pleroma.Activity
14 alias Pleroma.Keys
15 alias Pleroma.Notification
16 alias Pleroma.Object
17 alias Pleroma.Registration
18 alias Pleroma.Repo
19 alias Pleroma.RepoStreamer
20 alias Pleroma.User
21 alias Pleroma.Web
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
25 alias Pleroma.Web.OAuth
26 alias Pleroma.Web.OStatus
27 alias Pleroma.Web.RelMe
28 alias Pleroma.Web.Websub
29
30 require Logger
31
32 @type t :: %__MODULE__{}
33
34 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
35
36 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
37 @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])?)*$/
38
39 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
40 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
41
42 schema "users" do
43 field(:bio, :string)
44 field(:email, :string)
45 field(:name, :string)
46 field(:nickname, :string)
47 field(:password_hash, :string)
48 field(:password, :string, virtual: true)
49 field(:password_confirmation, :string, virtual: true)
50 field(:following, {:array, :string}, default: [])
51 field(:ap_id, :string)
52 field(:avatar, :map)
53 field(:local, :boolean, default: true)
54 field(:follower_address, :string)
55 field(:following_address, :string)
56 field(:search_rank, :float, virtual: true)
57 field(:search_type, :integer, virtual: true)
58 field(:tags, {:array, :string}, default: [])
59 field(:last_refreshed_at, :naive_datetime_usec)
60 field(:last_digest_emailed_at, :naive_datetime)
61 has_many(:notifications, Notification)
62 has_many(:registrations, Registration)
63 embeds_one(:info, User.Info)
64
65 timestamps()
66 end
67
68 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
69 do: !Pleroma.Config.get([:instance, :account_activation_required])
70
71 def auth_active?(%User{}), do: true
72
73 def visible_for?(user, for_user \\ nil)
74
75 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
76
77 def visible_for?(%User{} = user, for_user) do
78 auth_active?(user) || superuser?(for_user)
79 end
80
81 def visible_for?(_, _), do: false
82
83 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
84 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
85 def superuser?(_), do: false
86
87 def avatar_url(user, options \\ []) do
88 case user.avatar do
89 %{"url" => [%{"href" => href} | _]} -> href
90 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
91 end
92 end
93
94 def banner_url(user, options \\ []) do
95 case user.info.banner do
96 %{"url" => [%{"href" => href} | _]} -> href
97 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
98 end
99 end
100
101 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
102 def profile_url(%User{ap_id: ap_id}), do: ap_id
103 def profile_url(_), do: nil
104
105 def ap_id(%User{nickname: nickname}) do
106 "#{Web.base_url()}/users/#{nickname}"
107 end
108
109 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
110 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
111
112 @spec ap_following(User.t()) :: Sring.t()
113 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
114 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
115
116 def user_info(%User{} = user, args \\ %{}) do
117 following_count =
118 if args[:following_count], do: args[:following_count], else: following_count(user)
119
120 follower_count =
121 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
122
123 %{
124 note_count: user.info.note_count,
125 locked: user.info.locked,
126 confirmation_pending: user.info.confirmation_pending,
127 default_scope: user.info.default_scope
128 }
129 |> Map.put(:following_count, following_count)
130 |> Map.put(:follower_count, follower_count)
131 end
132
133 def set_info_cache(user, args) do
134 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
135 end
136
137 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
138 def restrict_deactivated(query) do
139 from(u in query,
140 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
141 )
142 end
143
144 def following_count(%User{following: []}), do: 0
145
146 def following_count(%User{} = user) do
147 user
148 |> get_friends_query()
149 |> Repo.aggregate(:count, :id)
150 end
151
152 def remote_user_creation(params) do
153 params =
154 params
155 |> Map.put(:info, params[:info] || %{})
156
157 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
158
159 changes =
160 %User{}
161 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
162 |> validate_required([:name, :ap_id])
163 |> unique_constraint(:nickname)
164 |> validate_format(:nickname, @email_regex)
165 |> validate_length(:bio, max: 5000)
166 |> validate_length(:name, max: 100)
167 |> put_change(:local, false)
168 |> put_embed(:info, info_cng)
169
170 if changes.valid? do
171 case info_cng.changes[:source_data] do
172 %{"followers" => followers, "following" => following} ->
173 changes
174 |> put_change(:follower_address, followers)
175 |> put_change(:following_address, following)
176
177 _ ->
178 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
179
180 changes
181 |> put_change(:follower_address, followers)
182 end
183 else
184 changes
185 end
186 end
187
188 def update_changeset(struct, params \\ %{}) do
189 struct
190 |> cast(params, [:bio, :name, :avatar, :following])
191 |> unique_constraint(:nickname)
192 |> validate_format(:nickname, local_nickname_regex())
193 |> validate_length(:bio, max: 5000)
194 |> validate_length(:name, min: 1, max: 100)
195 end
196
197 def upgrade_changeset(struct, params \\ %{}) do
198 params =
199 params
200 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
201
202 info_cng =
203 struct.info
204 |> User.Info.user_upgrade(params[:info])
205
206 struct
207 |> cast(params, [
208 :bio,
209 :name,
210 :follower_address,
211 :following_address,
212 :avatar,
213 :last_refreshed_at
214 ])
215 |> unique_constraint(:nickname)
216 |> validate_format(:nickname, local_nickname_regex())
217 |> validate_length(:bio, max: 5000)
218 |> validate_length(:name, max: 100)
219 |> put_embed(:info, info_cng)
220 end
221
222 def password_update_changeset(struct, params) do
223 struct
224 |> cast(params, [:password, :password_confirmation])
225 |> validate_required([:password, :password_confirmation])
226 |> validate_confirmation(:password)
227 |> put_password_hash
228 end
229
230 def reset_password(%User{id: user_id} = user, data) do
231 multi =
232 Multi.new()
233 |> Multi.update(:user, password_update_changeset(user, data))
234 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
235 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
236
237 case Repo.transaction(multi) do
238 {:ok, %{user: user} = _} -> set_cache(user)
239 {:error, _, changeset, _} -> {:error, changeset}
240 end
241 end
242
243 def register_changeset(struct, params \\ %{}, opts \\ []) do
244 need_confirmation? =
245 if is_nil(opts[:need_confirmation]) do
246 Pleroma.Config.get([:instance, :account_activation_required])
247 else
248 opts[:need_confirmation]
249 end
250
251 info_change =
252 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
253
254 changeset =
255 struct
256 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
257 |> validate_required([:name, :nickname, :password, :password_confirmation])
258 |> validate_confirmation(:password)
259 |> unique_constraint(:email)
260 |> unique_constraint(:nickname)
261 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
262 |> validate_format(:nickname, local_nickname_regex())
263 |> validate_format(:email, @email_regex)
264 |> validate_length(:bio, max: 1000)
265 |> validate_length(:name, min: 1, max: 100)
266 |> put_change(:info, info_change)
267
268 changeset =
269 if opts[:external] do
270 changeset
271 else
272 validate_required(changeset, [:email])
273 end
274
275 if changeset.valid? do
276 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
277 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
278
279 changeset
280 |> put_password_hash
281 |> put_change(:ap_id, ap_id)
282 |> unique_constraint(:ap_id)
283 |> put_change(:following, [followers])
284 |> put_change(:follower_address, followers)
285 else
286 changeset
287 end
288 end
289
290 defp autofollow_users(user) do
291 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
292
293 autofollowed_users =
294 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
295 |> Repo.all()
296
297 follow_all(user, autofollowed_users)
298 end
299
300 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
301 def register(%Ecto.Changeset{} = changeset) do
302 with {:ok, user} <- Repo.insert(changeset),
303 {:ok, user} <- autofollow_users(user),
304 {:ok, user} <- set_cache(user),
305 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
306 {:ok, _} <- try_send_confirmation_email(user) do
307 {:ok, user}
308 end
309 end
310
311 def try_send_confirmation_email(%User{} = user) do
312 if user.info.confirmation_pending &&
313 Pleroma.Config.get([:instance, :account_activation_required]) do
314 user
315 |> Pleroma.Emails.UserEmail.account_confirmation_email()
316 |> Pleroma.Emails.Mailer.deliver_async()
317
318 {:ok, :enqueued}
319 else
320 {:ok, :noop}
321 end
322 end
323
324 def needs_update?(%User{local: true}), do: false
325
326 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
327
328 def needs_update?(%User{local: false} = user) do
329 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
330 end
331
332 def needs_update?(_), do: true
333
334 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
335 {:ok, follower}
336 end
337
338 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
339 follow(follower, followed)
340 end
341
342 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
343 if not User.ap_enabled?(followed) do
344 follow(follower, followed)
345 else
346 {:ok, follower}
347 end
348 end
349
350 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
351 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
352 def follow_all(follower, followeds) do
353 followed_addresses =
354 followeds
355 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
356 |> Enum.map(fn %{follower_address: fa} -> fa end)
357
358 q =
359 from(u in User,
360 where: u.id == ^follower.id,
361 update: [
362 set: [
363 following:
364 fragment(
365 "array(select distinct unnest (array_cat(?, ?)))",
366 u.following,
367 ^followed_addresses
368 )
369 ]
370 ],
371 select: u
372 )
373
374 {1, [follower]} = Repo.update_all(q, [])
375
376 Enum.each(followeds, fn followed ->
377 update_follower_count(followed)
378 end)
379
380 set_cache(follower)
381 end
382
383 def follow(%User{} = follower, %User{info: info} = followed) do
384 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
385 ap_followers = followed.follower_address
386
387 cond do
388 info.deactivated ->
389 {:error, "Could not follow user: You are deactivated."}
390
391 deny_follow_blocked and blocks?(followed, follower) ->
392 {:error, "Could not follow user: #{followed.nickname} blocked you."}
393
394 true ->
395 if !followed.local && follower.local && !ap_enabled?(followed) do
396 Websub.subscribe(follower, followed)
397 end
398
399 q =
400 from(u in User,
401 where: u.id == ^follower.id,
402 update: [push: [following: ^ap_followers]],
403 select: u
404 )
405
406 {1, [follower]} = Repo.update_all(q, [])
407
408 {:ok, _} = update_follower_count(followed)
409
410 set_cache(follower)
411 end
412 end
413
414 def unfollow(%User{} = follower, %User{} = followed) do
415 ap_followers = followed.follower_address
416
417 if following?(follower, followed) and follower.ap_id != followed.ap_id do
418 q =
419 from(u in User,
420 where: u.id == ^follower.id,
421 update: [pull: [following: ^ap_followers]],
422 select: u
423 )
424
425 {1, [follower]} = Repo.update_all(q, [])
426
427 {:ok, followed} = update_follower_count(followed)
428
429 set_cache(follower)
430
431 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
432 else
433 {:error, "Not subscribed!"}
434 end
435 end
436
437 @spec following?(User.t(), User.t()) :: boolean
438 def following?(%User{} = follower, %User{} = followed) do
439 Enum.member?(follower.following, followed.follower_address)
440 end
441
442 def locked?(%User{} = user) do
443 user.info.locked || false
444 end
445
446 def get_by_id(id) do
447 Repo.get_by(User, id: id)
448 end
449
450 def get_by_ap_id(ap_id) do
451 Repo.get_by(User, ap_id: ap_id)
452 end
453
454 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
455 # of the ap_id and the domain and tries to get that user
456 def get_by_guessed_nickname(ap_id) do
457 domain = URI.parse(ap_id).host
458 name = List.last(String.split(ap_id, "/"))
459 nickname = "#{name}@#{domain}"
460
461 get_cached_by_nickname(nickname)
462 end
463
464 def set_cache({:ok, user}), do: set_cache(user)
465 def set_cache({:error, err}), do: {:error, err}
466
467 def set_cache(%User{} = user) do
468 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
469 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
470 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
471 {:ok, user}
472 end
473
474 def update_and_set_cache(changeset) do
475 with {:ok, user} <- Repo.update(changeset) do
476 set_cache(user)
477 else
478 e -> e
479 end
480 end
481
482 def invalidate_cache(user) do
483 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
484 Cachex.del(:user_cache, "nickname:#{user.nickname}")
485 Cachex.del(:user_cache, "user_info:#{user.id}")
486 end
487
488 def get_cached_by_ap_id(ap_id) do
489 key = "ap_id:#{ap_id}"
490 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
491 end
492
493 def get_cached_by_id(id) do
494 key = "id:#{id}"
495
496 ap_id =
497 Cachex.fetch!(:user_cache, key, fn _ ->
498 user = get_by_id(id)
499
500 if user do
501 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
502 {:commit, user.ap_id}
503 else
504 {:ignore, ""}
505 end
506 end)
507
508 get_cached_by_ap_id(ap_id)
509 end
510
511 def get_cached_by_nickname(nickname) do
512 key = "nickname:#{nickname}"
513
514 Cachex.fetch!(:user_cache, key, fn ->
515 user_result = get_or_fetch_by_nickname(nickname)
516
517 case user_result do
518 {:ok, user} -> {:commit, user}
519 {:error, _error} -> {:ignore, nil}
520 end
521 end)
522 end
523
524 def get_cached_by_nickname_or_id(nickname_or_id) do
525 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
526 end
527
528 def get_by_nickname(nickname) do
529 Repo.get_by(User, nickname: nickname) ||
530 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
531 Repo.get_by(User, nickname: local_nickname(nickname))
532 end
533 end
534
535 def get_by_email(email), do: Repo.get_by(User, email: email)
536
537 def get_by_nickname_or_email(nickname_or_email) do
538 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
539 end
540
541 def get_cached_user_info(user) do
542 key = "user_info:#{user.id}"
543 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
544 end
545
546 def fetch_by_nickname(nickname) do
547 ap_try = ActivityPub.make_user_from_nickname(nickname)
548
549 case ap_try do
550 {:ok, user} -> {:ok, user}
551 _ -> OStatus.make_user(nickname)
552 end
553 end
554
555 def get_or_fetch_by_nickname(nickname) do
556 with %User{} = user <- get_by_nickname(nickname) do
557 {:ok, user}
558 else
559 _e ->
560 with [_nick, _domain] <- String.split(nickname, "@"),
561 {:ok, user} <- fetch_by_nickname(nickname) do
562 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
563 fetch_initial_posts(user)
564 end
565
566 {:ok, user}
567 else
568 _e -> {:error, "not found " <> nickname}
569 end
570 end
571 end
572
573 @doc "Fetch some posts when the user has just been federated with"
574 def fetch_initial_posts(user),
575 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
576
577 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
578 def get_followers_query(%User{} = user, nil) do
579 User.Query.build(%{followers: user, deactivated: false})
580 end
581
582 def get_followers_query(user, page) do
583 from(u in get_followers_query(user, nil))
584 |> User.Query.paginate(page, 20)
585 end
586
587 @spec get_followers_query(User.t()) :: Ecto.Query.t()
588 def get_followers_query(user), do: get_followers_query(user, nil)
589
590 def get_followers(user, page \\ nil) do
591 q = get_followers_query(user, page)
592
593 {:ok, Repo.all(q)}
594 end
595
596 def get_followers_ids(user, page \\ nil) do
597 q = get_followers_query(user, page)
598
599 Repo.all(from(u in q, select: u.id))
600 end
601
602 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
603 def get_friends_query(%User{} = user, nil) do
604 User.Query.build(%{friends: user, deactivated: false})
605 end
606
607 def get_friends_query(user, page) do
608 from(u in get_friends_query(user, nil))
609 |> User.Query.paginate(page, 20)
610 end
611
612 @spec get_friends_query(User.t()) :: Ecto.Query.t()
613 def get_friends_query(user), do: get_friends_query(user, nil)
614
615 def get_friends(user, page \\ nil) do
616 q = get_friends_query(user, page)
617
618 {:ok, Repo.all(q)}
619 end
620
621 def get_friends_ids(user, page \\ nil) do
622 q = get_friends_query(user, page)
623
624 Repo.all(from(u in q, select: u.id))
625 end
626
627 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
628 def get_follow_requests(%User{} = user) do
629 users =
630 Activity.follow_requests_for_actor(user)
631 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
632 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
633 |> group_by([a, u], u.id)
634 |> select([a, u], u)
635 |> Repo.all()
636
637 {:ok, users}
638 end
639
640 def increase_note_count(%User{} = user) do
641 User
642 |> where(id: ^user.id)
643 |> update([u],
644 set: [
645 info:
646 fragment(
647 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
648 u.info,
649 u.info
650 )
651 ]
652 )
653 |> select([u], u)
654 |> Repo.update_all([])
655 |> case do
656 {1, [user]} -> set_cache(user)
657 _ -> {:error, user}
658 end
659 end
660
661 def decrease_note_count(%User{} = user) do
662 User
663 |> where(id: ^user.id)
664 |> update([u],
665 set: [
666 info:
667 fragment(
668 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
669 u.info,
670 u.info
671 )
672 ]
673 )
674 |> select([u], u)
675 |> Repo.update_all([])
676 |> case do
677 {1, [user]} -> set_cache(user)
678 _ -> {:error, user}
679 end
680 end
681
682 def update_note_count(%User{} = user) do
683 note_count_query =
684 from(
685 a in Object,
686 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
687 select: count(a.id)
688 )
689
690 note_count = Repo.one(note_count_query)
691
692 info_cng = User.Info.set_note_count(user.info, note_count)
693
694 user
695 |> change()
696 |> put_embed(:info, info_cng)
697 |> update_and_set_cache()
698 end
699
700 def update_follower_count(%User{} = user) do
701 follower_count_query =
702 User.Query.build(%{followers: user, deactivated: false})
703 |> select([u], %{count: count(u.id)})
704
705 User
706 |> where(id: ^user.id)
707 |> join(:inner, [u], s in subquery(follower_count_query))
708 |> update([u, s],
709 set: [
710 info:
711 fragment(
712 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
713 u.info,
714 s.count
715 )
716 ]
717 )
718 |> select([u], u)
719 |> Repo.update_all([])
720 |> case do
721 {1, [user]} -> set_cache(user)
722 _ -> {:error, user}
723 end
724 end
725
726 def remove_duplicated_following(%User{following: following} = user) do
727 uniq_following = Enum.uniq(following)
728
729 if length(following) == length(uniq_following) do
730 {:ok, user}
731 else
732 user
733 |> update_changeset(%{following: uniq_following})
734 |> update_and_set_cache()
735 end
736 end
737
738 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
739 def get_users_from_set(ap_ids, local_only \\ true) do
740 criteria = %{ap_id: ap_ids, deactivated: false}
741 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
742
743 User.Query.build(criteria)
744 |> Repo.all()
745 end
746
747 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
748 def get_recipients_from_activity(%Activity{recipients: to}) do
749 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
750 |> Repo.all()
751 end
752
753 def mute(muter, %User{ap_id: ap_id}) do
754 info_cng =
755 muter.info
756 |> User.Info.add_to_mutes(ap_id)
757
758 cng =
759 change(muter)
760 |> put_embed(:info, info_cng)
761
762 update_and_set_cache(cng)
763 end
764
765 def unmute(muter, %{ap_id: ap_id}) do
766 info_cng =
767 muter.info
768 |> User.Info.remove_from_mutes(ap_id)
769
770 cng =
771 change(muter)
772 |> put_embed(:info, info_cng)
773
774 update_and_set_cache(cng)
775 end
776
777 def subscribe(subscriber, %{ap_id: ap_id}) do
778 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
779
780 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
781 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
782
783 if blocked do
784 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
785 else
786 info_cng =
787 subscribed.info
788 |> User.Info.add_to_subscribers(subscriber.ap_id)
789
790 change(subscribed)
791 |> put_embed(:info, info_cng)
792 |> update_and_set_cache()
793 end
794 end
795 end
796
797 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
798 with %User{} = user <- get_cached_by_ap_id(ap_id) do
799 info_cng =
800 user.info
801 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
802
803 change(user)
804 |> put_embed(:info, info_cng)
805 |> update_and_set_cache()
806 end
807 end
808
809 def block(blocker, %User{ap_id: ap_id} = blocked) do
810 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
811 blocker =
812 if following?(blocker, blocked) do
813 {:ok, blocker, _} = unfollow(blocker, blocked)
814 blocker
815 else
816 blocker
817 end
818
819 blocker =
820 if subscribed_to?(blocked, blocker) do
821 {:ok, blocker} = unsubscribe(blocked, blocker)
822 blocker
823 else
824 blocker
825 end
826
827 if following?(blocked, blocker) do
828 unfollow(blocked, blocker)
829 end
830
831 {:ok, blocker} = update_follower_count(blocker)
832
833 info_cng =
834 blocker.info
835 |> User.Info.add_to_block(ap_id)
836
837 cng =
838 change(blocker)
839 |> put_embed(:info, info_cng)
840
841 update_and_set_cache(cng)
842 end
843
844 # helper to handle the block given only an actor's AP id
845 def block(blocker, %{ap_id: ap_id}) do
846 block(blocker, get_cached_by_ap_id(ap_id))
847 end
848
849 def unblock(blocker, %{ap_id: ap_id}) do
850 info_cng =
851 blocker.info
852 |> User.Info.remove_from_block(ap_id)
853
854 cng =
855 change(blocker)
856 |> put_embed(:info, info_cng)
857
858 update_and_set_cache(cng)
859 end
860
861 def mutes?(nil, _), do: false
862 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
863
864 def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
865 blocks = info.blocks
866 domain_blocks = info.domain_blocks
867 %{host: host} = URI.parse(ap_id)
868
869 Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
870 end
871
872 def subscribed_to?(user, %{ap_id: ap_id}) do
873 with %User{} = target <- get_cached_by_ap_id(ap_id) do
874 Enum.member?(target.info.subscribers, user.ap_id)
875 end
876 end
877
878 @spec muted_users(User.t()) :: [User.t()]
879 def muted_users(user) do
880 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
881 |> Repo.all()
882 end
883
884 @spec blocked_users(User.t()) :: [User.t()]
885 def blocked_users(user) do
886 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
887 |> Repo.all()
888 end
889
890 @spec subscribers(User.t()) :: [User.t()]
891 def subscribers(user) do
892 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
893 |> Repo.all()
894 end
895
896 def block_domain(user, domain) do
897 info_cng =
898 user.info
899 |> User.Info.add_to_domain_block(domain)
900
901 cng =
902 change(user)
903 |> put_embed(:info, info_cng)
904
905 update_and_set_cache(cng)
906 end
907
908 def unblock_domain(user, domain) do
909 info_cng =
910 user.info
911 |> User.Info.remove_from_domain_block(domain)
912
913 cng =
914 change(user)
915 |> put_embed(:info, info_cng)
916
917 update_and_set_cache(cng)
918 end
919
920 def deactivate_async(user, status \\ true) do
921 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
922 end
923
924 def deactivate(%User{} = user, status \\ true) do
925 info_cng = User.Info.set_activation_status(user.info, status)
926
927 with {:ok, friends} <- User.get_friends(user),
928 {:ok, followers} <- User.get_followers(user),
929 {:ok, user} <-
930 user
931 |> change()
932 |> put_embed(:info, info_cng)
933 |> update_and_set_cache() do
934 Enum.each(followers, &invalidate_cache(&1))
935 Enum.each(friends, &update_follower_count(&1))
936
937 {:ok, user}
938 end
939 end
940
941 def update_notification_settings(%User{} = user, settings \\ %{}) do
942 info_changeset = User.Info.update_notification_settings(user.info, settings)
943
944 change(user)
945 |> put_embed(:info, info_changeset)
946 |> update_and_set_cache()
947 end
948
949 @spec delete(User.t()) :: :ok
950 def delete(%User{} = user),
951 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
952
953 @spec perform(atom(), User.t()) :: {:ok, User.t()}
954 def perform(:delete, %User{} = user) do
955 {:ok, _user} = ActivityPub.delete(user)
956
957 # Remove all relationships
958 {:ok, followers} = User.get_followers(user)
959
960 Enum.each(followers, fn follower ->
961 ActivityPub.unfollow(follower, user)
962 User.unfollow(follower, user)
963 end)
964
965 {:ok, friends} = User.get_friends(user)
966
967 Enum.each(friends, fn followed ->
968 ActivityPub.unfollow(user, followed)
969 User.unfollow(user, followed)
970 end)
971
972 delete_user_activities(user)
973 invalidate_cache(user)
974 Repo.delete(user)
975 end
976
977 @spec perform(atom(), User.t()) :: {:ok, User.t()}
978 def perform(:fetch_initial_posts, %User{} = user) do
979 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
980
981 Enum.each(
982 # Insert all the posts in reverse order, so they're in the right order on the timeline
983 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
984 &Pleroma.Web.Federator.incoming_ap_doc/1
985 )
986
987 {:ok, user}
988 end
989
990 def perform(:deactivate_async, user, status), do: deactivate(user, status)
991
992 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
993 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
994 when is_list(blocked_identifiers) do
995 Enum.map(
996 blocked_identifiers,
997 fn blocked_identifier ->
998 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
999 {:ok, blocker} <- block(blocker, blocked),
1000 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1001 blocked
1002 else
1003 err ->
1004 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1005 err
1006 end
1007 end
1008 )
1009 end
1010
1011 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1012 def perform(:follow_import, %User{} = follower, followed_identifiers)
1013 when is_list(followed_identifiers) do
1014 Enum.map(
1015 followed_identifiers,
1016 fn followed_identifier ->
1017 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1018 {:ok, follower} <- maybe_direct_follow(follower, followed),
1019 {:ok, _} <- ActivityPub.follow(follower, followed) do
1020 followed
1021 else
1022 err ->
1023 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1024 err
1025 end
1026 end
1027 )
1028 end
1029
1030 @spec external_users_query() :: Ecto.Query.t()
1031 def external_users_query do
1032 User.Query.build(%{
1033 external: true,
1034 active: true,
1035 order_by: :id
1036 })
1037 end
1038
1039 @spec external_users(keyword()) :: [User.t()]
1040 def external_users(opts \\ []) do
1041 query =
1042 external_users_query()
1043 |> select([u], struct(u, [:id, :ap_id, :info]))
1044
1045 query =
1046 if opts[:max_id],
1047 do: where(query, [u], u.id > ^opts[:max_id]),
1048 else: query
1049
1050 query =
1051 if opts[:limit],
1052 do: limit(query, ^opts[:limit]),
1053 else: query
1054
1055 Repo.all(query)
1056 end
1057
1058 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1059 do:
1060 PleromaJobQueue.enqueue(:background, __MODULE__, [
1061 :blocks_import,
1062 blocker,
1063 blocked_identifiers
1064 ])
1065
1066 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1067 do:
1068 PleromaJobQueue.enqueue(:background, __MODULE__, [
1069 :follow_import,
1070 follower,
1071 followed_identifiers
1072 ])
1073
1074 def delete_user_activities(%User{ap_id: ap_id} = user) do
1075 ap_id
1076 |> Activity.query_by_actor()
1077 |> RepoStreamer.chunk_stream(50)
1078 |> Stream.each(fn activities ->
1079 Enum.each(activities, &delete_activity(&1))
1080 end)
1081 |> Stream.run()
1082
1083 {:ok, user}
1084 end
1085
1086 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1087 activity
1088 |> Object.normalize()
1089 |> ActivityPub.delete()
1090 end
1091
1092 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1093 user = get_cached_by_ap_id(activity.actor)
1094 object = Object.normalize(activity)
1095
1096 ActivityPub.unlike(user, object)
1097 end
1098
1099 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1100 user = get_cached_by_ap_id(activity.actor)
1101 object = Object.normalize(activity)
1102
1103 ActivityPub.unannounce(user, object)
1104 end
1105
1106 defp delete_activity(_activity), do: "Doing nothing"
1107
1108 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1109 Pleroma.HTML.Scrubber.TwitterText
1110 end
1111
1112 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1113
1114 def fetch_by_ap_id(ap_id) do
1115 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1116
1117 case ap_try do
1118 {:ok, user} ->
1119 {:ok, user}
1120
1121 _ ->
1122 case OStatus.make_user(ap_id) do
1123 {:ok, user} -> {:ok, user}
1124 _ -> {:error, "Could not fetch by AP id"}
1125 end
1126 end
1127 end
1128
1129 def get_or_fetch_by_ap_id(ap_id) do
1130 user = get_cached_by_ap_id(ap_id)
1131
1132 if !is_nil(user) and !User.needs_update?(user) do
1133 {:ok, user}
1134 else
1135 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1136 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1137
1138 resp = fetch_by_ap_id(ap_id)
1139
1140 if should_fetch_initial do
1141 with {:ok, %User{} = user} <- resp do
1142 fetch_initial_posts(user)
1143 end
1144 end
1145
1146 resp
1147 end
1148 end
1149
1150 def get_or_create_instance_user do
1151 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1152
1153 if user = get_cached_by_ap_id(relay_uri) do
1154 user
1155 else
1156 changes =
1157 %User{info: %User.Info{}}
1158 |> cast(%{}, [:ap_id, :nickname, :local])
1159 |> put_change(:ap_id, relay_uri)
1160 |> put_change(:nickname, nil)
1161 |> put_change(:local, true)
1162 |> put_change(:follower_address, relay_uri <> "/followers")
1163
1164 {:ok, user} = Repo.insert(changes)
1165 user
1166 end
1167 end
1168
1169 # AP style
1170 def public_key_from_info(%{
1171 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1172 }) do
1173 key =
1174 public_key_pem
1175 |> :public_key.pem_decode()
1176 |> hd()
1177 |> :public_key.pem_entry_decode()
1178
1179 {:ok, key}
1180 end
1181
1182 # OStatus Magic Key
1183 def public_key_from_info(%{magic_key: magic_key}) do
1184 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1185 end
1186
1187 def get_public_key_for_ap_id(ap_id) do
1188 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1189 {:ok, public_key} <- public_key_from_info(user.info) do
1190 {:ok, public_key}
1191 else
1192 _ -> :error
1193 end
1194 end
1195
1196 defp blank?(""), do: nil
1197 defp blank?(n), do: n
1198
1199 def insert_or_update_user(data) do
1200 data
1201 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1202 |> remote_user_creation()
1203 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1204 |> set_cache()
1205 end
1206
1207 def ap_enabled?(%User{local: true}), do: true
1208 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1209 def ap_enabled?(_), do: false
1210
1211 @doc "Gets or fetch a user by uri or nickname."
1212 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1213 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1214 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1215
1216 # wait a period of time and return newest version of the User structs
1217 # this is because we have synchronous follow APIs and need to simulate them
1218 # with an async handshake
1219 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1220 with %User{} = a <- User.get_cached_by_id(a.id),
1221 %User{} = b <- User.get_cached_by_id(b.id) do
1222 {:ok, a, b}
1223 else
1224 _e ->
1225 :error
1226 end
1227 end
1228
1229 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1230 with :ok <- :timer.sleep(timeout),
1231 %User{} = a <- User.get_cached_by_id(a.id),
1232 %User{} = b <- User.get_cached_by_id(b.id) do
1233 {:ok, a, b}
1234 else
1235 _e ->
1236 :error
1237 end
1238 end
1239
1240 def parse_bio(bio) when is_binary(bio) and bio != "" do
1241 bio
1242 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1243 |> elem(0)
1244 end
1245
1246 def parse_bio(_), do: ""
1247
1248 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1249 # TODO: get profile URLs other than user.ap_id
1250 profile_urls = [user.ap_id]
1251
1252 bio
1253 |> CommonUtils.format_input("text/plain",
1254 mentions_format: :full,
1255 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1256 )
1257 |> elem(0)
1258 end
1259
1260 def parse_bio(_, _), do: ""
1261
1262 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1263 Repo.transaction(fn ->
1264 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1265 end)
1266 end
1267
1268 def tag(nickname, tags) when is_binary(nickname),
1269 do: tag(get_by_nickname(nickname), tags)
1270
1271 def tag(%User{} = user, tags),
1272 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1273
1274 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1275 Repo.transaction(fn ->
1276 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1277 end)
1278 end
1279
1280 def untag(nickname, tags) when is_binary(nickname),
1281 do: untag(get_by_nickname(nickname), tags)
1282
1283 def untag(%User{} = user, tags),
1284 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1285
1286 defp update_tags(%User{} = user, new_tags) do
1287 {:ok, updated_user} =
1288 user
1289 |> change(%{tags: new_tags})
1290 |> update_and_set_cache()
1291
1292 updated_user
1293 end
1294
1295 defp normalize_tags(tags) do
1296 [tags]
1297 |> List.flatten()
1298 |> Enum.map(&String.downcase(&1))
1299 end
1300
1301 defp local_nickname_regex do
1302 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1303 @extended_local_nickname_regex
1304 else
1305 @strict_local_nickname_regex
1306 end
1307 end
1308
1309 def local_nickname(nickname_or_mention) do
1310 nickname_or_mention
1311 |> full_nickname()
1312 |> String.split("@")
1313 |> hd()
1314 end
1315
1316 def full_nickname(nickname_or_mention),
1317 do: String.trim_leading(nickname_or_mention, "@")
1318
1319 def error_user(ap_id) do
1320 %User{
1321 name: ap_id,
1322 ap_id: ap_id,
1323 info: %User.Info{},
1324 nickname: "erroruser@example.com",
1325 inserted_at: NaiveDateTime.utc_now()
1326 }
1327 end
1328
1329 @spec all_superusers() :: [User.t()]
1330 def all_superusers do
1331 User.Query.build(%{super_users: true, local: true, deactivated: false})
1332 |> Repo.all()
1333 end
1334
1335 def showing_reblogs?(%User{} = user, %User{} = target) do
1336 target.ap_id not in user.info.muted_reblogs
1337 end
1338
1339 @doc """
1340 The function returns a query to get users with no activity for given interval of days.
1341 Inactive users are those who didn't read any notification, or had any activity where
1342 the user is the activity's actor, during `inactivity_threshold` days.
1343 Deactivated users will not appear in this list.
1344
1345 ## Examples
1346
1347 iex> Pleroma.User.list_inactive_users()
1348 %Ecto.Query{}
1349 """
1350 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1351 def list_inactive_users_query(inactivity_threshold \\ 7) do
1352 negative_inactivity_threshold = -inactivity_threshold
1353 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1354 # Subqueries are not supported in `where` clauses, join gets too complicated.
1355 has_read_notifications =
1356 from(n in Pleroma.Notification,
1357 where: n.seen == true,
1358 group_by: n.id,
1359 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1360 select: n.user_id
1361 )
1362 |> Pleroma.Repo.all()
1363
1364 from(u in Pleroma.User,
1365 left_join: a in Pleroma.Activity,
1366 on: u.ap_id == a.actor,
1367 where: not is_nil(u.nickname),
1368 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1369 where: u.id not in ^has_read_notifications,
1370 group_by: u.id,
1371 having:
1372 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1373 is_nil(max(a.inserted_at))
1374 )
1375 end
1376
1377 @doc """
1378 Enable or disable email notifications for user
1379
1380 ## Examples
1381
1382 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1383 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1384
1385 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1386 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1387 """
1388 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1389 {:ok, t()} | {:error, Ecto.Changeset.t()}
1390 def switch_email_notifications(user, type, status) do
1391 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1392
1393 change(user)
1394 |> put_embed(:info, info)
1395 |> update_and_set_cache()
1396 end
1397
1398 @doc """
1399 Set `last_digest_emailed_at` value for the user to current time
1400 """
1401 @spec touch_last_digest_emailed_at(t()) :: t()
1402 def touch_last_digest_emailed_at(user) do
1403 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1404
1405 {:ok, updated_user} =
1406 user
1407 |> change(%{last_digest_emailed_at: now})
1408 |> update_and_set_cache()
1409
1410 updated_user
1411 end
1412
1413 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1414 def toggle_confirmation(%User{} = user) do
1415 need_confirmation? = !user.info.confirmation_pending
1416
1417 info_changeset =
1418 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1419
1420 user
1421 |> change()
1422 |> put_embed(:info, info_changeset)
1423 |> update_and_set_cache()
1424 end
1425
1426 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1427 mascot
1428 end
1429
1430 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1431 # use instance-default
1432 config = Pleroma.Config.get([:assets, :mascots])
1433 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1434 mascot = Keyword.get(config, default_mascot)
1435
1436 %{
1437 "id" => "default-mascot",
1438 "url" => mascot[:url],
1439 "preview_url" => mascot[:url],
1440 "pleroma" => %{
1441 "mime_type" => mascot[:mime_type]
1442 }
1443 }
1444 end
1445
1446 def ensure_keys_present(user) do
1447 info = user.info
1448
1449 if info.keys do
1450 {:ok, user}
1451 else
1452 {:ok, pem} = Keys.generate_rsa_pem()
1453
1454 info_cng =
1455 info
1456 |> User.Info.set_keys(pem)
1457
1458 cng =
1459 Ecto.Changeset.change(user)
1460 |> Ecto.Changeset.put_embed(:info, info_cng)
1461
1462 update_and_set_cache(cng)
1463 end
1464 end
1465
1466 def get_ap_ids_by_nicknames(nicknames) do
1467 from(u in User,
1468 where: u.nickname in ^nicknames,
1469 select: u.ap_id
1470 )
1471 |> Repo.all()
1472 end
1473
1474 defdelegate search(query, opts \\ []), to: User.Search
1475
1476 defp put_password_hash(
1477 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1478 ) do
1479 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1480 end
1481
1482 defp put_password_hash(changeset), do: changeset
1483 end