using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using EntityFrameworkCore.Projectables; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Helpers; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Database.Tables; [Table("user")] [Index("Host")] [Index("UsernameLower", "Host", IsUnique = true)] [Index("UpdatedAt")] [Index("UsernameLower")] [Index("Uri")] [Index("LastActiveDate")] [Index("IsExplorable")] [Index("IsAdmin")] [Index("IsModerator")] [Index("CreatedAt")] [Index("Tags")] [Index("AvatarId", IsUnique = true)] [Index("BannerId", IsUnique = true)] [Index("Token", IsUnique = true)] public class User : IEntity { /// /// The created date of the User. /// [Column("createdAt")] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; /// /// The updated date of the User. /// [Column("updatedAt")] public DateTime? UpdatedAt { get; set; } [Column("lastFetchedAt")] public DateTime? LastFetchedAt { get; set; } [NotMapped] [Projectable] public bool NeedsUpdate => Host != null && (LastFetchedAt == null || LastFetchedAt < DateTime.Now - TimeSpan.FromHours(24)); /// /// The username of the User. /// [Column("username")] [StringLength(128)] public string Username { get; set; } = null!; /// /// The username (lowercased) of the User. /// [Column("usernameLower")] [StringLength(128)] public string UsernameLower { get; set; } = null!; /// /// The name of the User. /// [Column("name")] [StringLength(128)] public string? DisplayName { get; set; } /// /// The count of followers. /// [Column("followersCount")] public int FollowersCount { get; set; } /// /// The count of following. /// [Column("followingCount")] public int FollowingCount { get; set; } /// /// The count of notes. /// [Column("notesCount")] public int NotesCount { get; set; } /// /// The ID of avatar DriveFile. /// [Column("avatarId")] [StringLength(32)] public string? AvatarId { get; set; } /// /// The ID of banner DriveFile. /// [Column("bannerId")] [StringLength(32)] public string? BannerId { get; set; } [Column("tags", TypeName = "character varying(128)[]")] public List Tags { get; set; } = []; /// /// Whether the User is suspended. /// [Column("isSuspended")] public bool IsSuspended { get; set; } /// /// Whether the User is silenced. /// [Column("isSilenced")] public bool IsSilenced { get; set; } /// /// Whether the User is locked. /// [Column("isLocked")] public bool IsLocked { get; set; } /// /// Whether the User is a bot. /// [Column("isBot")] public bool IsBot { get; set; } /// /// Whether the User is a cat. /// [Column("isCat")] public bool IsCat { get; set; } /// /// Whether the User is the admin. /// [Column("isAdmin")] public bool IsAdmin { get; set; } /// /// Whether the User is a moderator. /// [Column("isModerator")] public bool IsModerator { get; set; } [Column("emojis", TypeName = "character varying(128)[]")] public List Emojis { get; set; } = []; /// /// The host of the User. It will be null if the origin of the user is local. /// [Column("host")] [StringLength(512)] public string? Host { get; set; } [NotMapped] [Projectable] public string Acct => Username + (Host != null ? "@" + Host : ""); [NotMapped] [Projectable] public string AcctWithPrefix => "acct:" + Acct; /// /// The inbox URL of the User. It will be null if the origin of the user is local. /// [Column("inbox")] [StringLength(512)] public string? Inbox { get; set; } /// /// The sharedInbox URL of the User. It will be null if the origin of the user is local. /// [Column("sharedInbox")] [StringLength(512)] public string? SharedInbox { get; set; } /// /// The featured URL of the User. It will be null if the origin of the user is local. /// [Column("featured")] [StringLength(512)] public string? Featured { get; set; } /// /// The URI of the User. It will be null if the origin of the user is local. /// [Column("uri")] [StringLength(512)] public string? Uri { get; set; } /// /// The native access token of the User. It will be null if the origin of the user is local. /// [Column("token")] [StringLength(16)] public string? Token { get; set; } /// /// Whether the User is explorable. /// [Column("isExplorable")] public bool IsExplorable { get; set; } /// /// The URI of the user Follower Collection. It will be null if the origin of the user is local. /// [Column("followersUri")] [StringLength(512)] public string? FollowersUri { get; set; } [Column("lastActiveDate")] public DateTime? LastActiveDate { get; set; } [Column("hideOnlineStatus")] public bool HideOnlineStatus { get; set; } /// /// Whether the User is deleted. /// [Column("isDeleted")] public bool IsDeleted { get; set; } /// /// Overrides user drive capacity limit /// [Column("driveCapacityOverrideMb")] public int? DriveCapacityOverrideMb { get; set; } /// /// The URI of the new account of the User /// [Column("movedToUri")] [StringLength(512)] public string? MovedToUri { get; set; } /// /// URIs the user is known as too /// [Column("alsoKnownAs")] public List? AlsoKnownAs { get; set; } /// /// Whether to speak as a cat if isCat. /// [Column("speakAsCat")] public bool SpeakAsCat { get; set; } /// /// The URL of the avatar DriveFile /// [Column("avatarUrl")] [StringLength(512)] public string? AvatarUrl { get; set; } /// /// The blurhash of the avatar DriveFile /// [Column("avatarBlurhash")] [StringLength(128)] public string? AvatarBlurhash { get; set; } /// /// The URL of the banner DriveFile /// [Column("bannerUrl")] [StringLength(512)] public string? BannerUrl { get; set; } /// /// The blurhash of the banner DriveFile /// [Column("bannerBlurhash")] [StringLength(128)] public string? BannerBlurhash { get; set; } [InverseProperty(nameof(AbuseUserReport.Assignee))] public virtual ICollection AbuseUserReportAssignees { get; set; } = new List(); [InverseProperty(nameof(AbuseUserReport.Reporter))] public virtual ICollection AbuseUserReportReporters { get; set; } = new List(); [InverseProperty(nameof(AbuseUserReport.TargetUser))] public virtual ICollection AbuseUserReportTargetUsers { get; set; } = new List(); [InverseProperty(nameof(AccessToken.User))] public virtual ICollection AccessTokens { get; set; } = new List(); [InverseProperty(nameof(AnnouncementRead.User))] public virtual ICollection AnnouncementReads { get; set; } = new List(); [InverseProperty(nameof(Antenna.User))] public virtual ICollection Antennas { get; set; } = new List(); [InverseProperty(nameof(App.User))] public virtual ICollection Apps { get; set; } = new List(); [InverseProperty(nameof(AttestationChallenge.User))] public virtual ICollection AttestationChallenges { get; set; } = new List(); [InverseProperty(nameof(AuthSession.User))] public virtual ICollection AuthSessions { get; set; } = new List(); [ForeignKey("AvatarId")] [InverseProperty(nameof(DriveFile.UserAvatar))] public virtual DriveFile? Avatar { get; set; } [ForeignKey("BannerId")] [InverseProperty(nameof(DriveFile.UserBanner))] public virtual DriveFile? Banner { get; set; } [InverseProperty(nameof(Tables.Blocking.Blockee))] public virtual ICollection IncomingBlocks { get; set; } = new List(); [InverseProperty(nameof(Tables.Blocking.Blocker))] public virtual ICollection OutgoingBlocks { get; set; } = new List(); [NotMapped] [Projectable] public virtual IEnumerable BlockedBy => IncomingBlocks.Select(p => p.Blocker); [NotMapped] [Projectable] public virtual IEnumerable Blocking => OutgoingBlocks.Select(p => p.Blockee); [InverseProperty(nameof(ChannelFollowing.Follower))] public virtual ICollection ChannelFollowings { get; set; } = new List(); [InverseProperty(nameof(Channel.User))] public virtual ICollection Channels { get; set; } = new List(); [InverseProperty(nameof(Clip.User))] public virtual ICollection Clips { get; set; } = new List(); [InverseProperty(nameof(DriveFile.User))] public virtual ICollection DriveFiles { get; set; } = new List(); [InverseProperty(nameof(DriveFolder.User))] public virtual ICollection DriveFolders { get; set; } = new List(); [InverseProperty(nameof(FollowRequest.Followee))] public virtual ICollection IncomingFollowRequests { get; set; } = new List(); [InverseProperty(nameof(FollowRequest.Follower))] public virtual ICollection OutgoingFollowRequests { get; set; } = new List(); [NotMapped] [Projectable] public virtual IEnumerable ReceivedFollowRequests => IncomingFollowRequests.Select(p => p.Follower); [NotMapped] [Projectable] public virtual IEnumerable SentFollowRequests => OutgoingFollowRequests.Select(p => p.Followee); [InverseProperty(nameof(Tables.Following.Followee))] public virtual ICollection IncomingFollowRelationships { get; set; } = new List(); [InverseProperty(nameof(Tables.Following.Follower))] public virtual ICollection OutgoingFollowRelationships { get; set; } = new List(); [NotMapped] [Projectable] public virtual IEnumerable Followers => IncomingFollowRelationships.Select(p => p.Follower); [NotMapped] [Projectable] public virtual IEnumerable Following => OutgoingFollowRelationships.Select(p => p.Followee); [InverseProperty(nameof(GalleryLike.User))] public virtual ICollection GalleryLikes { get; set; } = new List(); [InverseProperty(nameof(GalleryPost.User))] public virtual ICollection GalleryPosts { get; set; } = new List(); [InverseProperty(nameof(Tables.HtmlUserCacheEntry.User))] public virtual HtmlUserCacheEntry? HtmlUserCacheEntry { get; set; } [InverseProperty(nameof(Marker.User))] public virtual ICollection Markers { get; set; } = new List(); [InverseProperty(nameof(MessagingMessage.Recipient))] public virtual ICollection MessagingMessageRecipients { get; set; } = new List(); [InverseProperty(nameof(MessagingMessage.User))] public virtual ICollection MessagingMessageUsers { get; set; } = new List(); [InverseProperty(nameof(ModerationLog.User))] public virtual ICollection ModerationLogs { get; set; } = new List(); [InverseProperty(nameof(Tables.Muting.Mutee))] public virtual ICollection IncomingMutes { get; set; } = new List(); [InverseProperty(nameof(Tables.Muting.Muter))] public virtual ICollection OutgoingMutes { get; set; } = new List(); [NotMapped] [Projectable] public virtual IEnumerable MutedBy => IncomingMutes.Select(p => p.Muter); [NotMapped] [Projectable] public virtual IEnumerable Muting => OutgoingMutes.Select(p => p.Mutee); [InverseProperty(nameof(NoteBookmark.User))] public virtual ICollection NoteBookmarks { get; set; } = new List(); [NotMapped] [Projectable] public virtual IEnumerable BookmarkedNotes => NoteBookmarks.Select(p => p.Note); [InverseProperty(nameof(NoteReaction.User))] public virtual ICollection NoteLikes { get; set; } = new List(); [NotMapped] [Projectable] public virtual IEnumerable LikedNotes => NoteLikes.Select(p => p.Note); [InverseProperty(nameof(NoteReaction.User))] public virtual ICollection NoteReactions { get; set; } = new List(); [NotMapped] [Projectable] public virtual IEnumerable ReactedNotes => NoteReactions.Select(p => p.Note).Distinct(); [InverseProperty(nameof(NoteThreadMuting.User))] public virtual ICollection NoteThreadMutings { get; set; } = new List(); [InverseProperty(nameof(NoteUnread.User))] public virtual ICollection NoteUnreads { get; set; } = new List(); [InverseProperty(nameof(NoteWatching.User))] public virtual ICollection NoteWatchings { get; set; } = new List(); [InverseProperty(nameof(Note.User))] public virtual ICollection Notes { get; set; } = new List(); [InverseProperty(nameof(Notification.Notifiee))] public virtual ICollection NotificationNotifiees { get; set; } = new List(); [InverseProperty(nameof(Notification.Notifier))] public virtual ICollection NotificationNotifiers { get; set; } = new List(); [InverseProperty(nameof(OauthToken.User))] public virtual ICollection OauthTokens { get; set; } = new List(); [InverseProperty(nameof(PageLike.User))] public virtual ICollection PageLikes { get; set; } = new List(); [InverseProperty(nameof(Page.User))] public virtual ICollection Pages { get; set; } = new List(); [InverseProperty(nameof(PasswordResetRequest.User))] public virtual ICollection PasswordResetRequests { get; set; } = new List(); [InverseProperty(nameof(PollVote.User))] public virtual ICollection PollVotes { get; set; } = new List(); [InverseProperty(nameof(PromoRead.User))] public virtual ICollection PromoReads { get; set; } = new List(); [InverseProperty(nameof(RegistryItem.User))] public virtual ICollection RegistryItems { get; set; } = new List(); [InverseProperty(nameof(RenoteMuting.Mutee))] public virtual ICollection RenoteMutingMutees { get; set; } = new List(); [InverseProperty(nameof(RenoteMuting.Muter))] public virtual ICollection RenoteMutingMuters { get; set; } = new List(); [InverseProperty(nameof(Session.User))] public virtual ICollection Sessions { get; set; } = new List(); [InverseProperty(nameof(Signin.User))] public virtual ICollection Signins { get; set; } = new List(); [InverseProperty(nameof(SwSubscription.User))] public virtual ICollection SwSubscriptions { get; set; } = new List(); [InverseProperty(nameof(UserGroupInvitation.User))] public virtual ICollection UserGroupInvitations { get; set; } = new List(); [InverseProperty(nameof(UserGroupMember.User))] public virtual ICollection UserGroupMemberships { get; set; } = new List(); [InverseProperty(nameof(UserGroup.User))] public virtual ICollection UserGroups { get; set; } = new List(); [InverseProperty(nameof(Tables.UserKeypair.User))] public virtual UserKeypair? UserKeypair { get; set; } [InverseProperty(nameof(UserListMember.User))] public virtual ICollection UserListMembers { get; set; } = new List(); [InverseProperty(nameof(UserList.User))] public virtual ICollection UserLists { get; set; } = new List(); [InverseProperty(nameof(UserNotePin.User))] public virtual ICollection UserNotePins { get; set; } = new List(); [NotMapped] [Projectable] public virtual IEnumerable PinnedNotes => UserNotePins.Select(p => p.Note); [InverseProperty(nameof(Tables.UserProfile.User))] public virtual UserProfile? UserProfile { get; set; } [InverseProperty(nameof(Tables.UserPublickey.User))] public virtual UserPublickey? UserPublickey { get; set; } [InverseProperty(nameof(Tables.UserSettings.User))] public virtual UserSettings? UserSettings { get; set; } [InverseProperty(nameof(UserSecurityKey.User))] public virtual ICollection UserSecurityKeys { get; set; } = new List(); [InverseProperty(nameof(Webhook.User))] public virtual ICollection Webhooks { get; set; } = new List(); [NotMapped] public bool? PrecomputedIsBlocking { get; set; } [NotMapped] public bool? PrecomputedIsBlockedBy { get; set; } [NotMapped] public bool? PrecomputedIsMuting { get; set; } [NotMapped] public bool? PrecomputedIsMutedBy { get; set; } [NotMapped] public bool? PrecomputedIsFollowing { get; set; } [NotMapped] public bool? PrecomputedIsFollowedBy { get; set; } [NotMapped] public bool? PrecomputedIsRequested { get; set; } [NotMapped] public bool? PrecomputedIsRequestedBy { get; set; } [Key] [Column("id")] [StringLength(32)] public string Id { get; set; } = IdHelpers.GenerateSlowflakeId(); [Projectable] public bool DisplayNameContainsCaseInsensitive(string str) => DisplayName != null && EF.Functions.ILike(DisplayName, "%" + EfHelpers.EscapeLikeQuery(str) + "%", @"\"); [Projectable] public bool UsernameContainsCaseInsensitive(string str) => UsernameLower.Contains(str); [Projectable] public bool IsBlockedBy(User user) => BlockedBy.Contains(user); [Projectable] public bool IsBlocking(User user) => Blocking.Contains(user); [Projectable] public bool IsFollowedBy(User user) => Followers.Contains(user); [Projectable] public bool IsFollowing(User user) => Following.Contains(user); [Projectable] public bool IsRequestedBy(User user) => ReceivedFollowRequests.Contains(user); [Projectable] public bool IsRequested(User user) => SentFollowRequests.Contains(user); [Projectable] public bool IsMutedBy(User user) => MutedBy.Contains(user); [Projectable] public bool IsMuting(User user) => Muting.Contains(user); [Projectable] public bool HasPinned(Note note) => PinnedNotes.Contains(note); [Projectable] public bool HasBookmarked(Note note) => BookmarkedNotes.Contains(note); [Projectable] public bool HasLiked(Note note) => LikedNotes.Contains(note); [Projectable] public bool HasReacted(Note note) => ReactedNotes.Contains(note); [Projectable] public bool HasRenoted(Note note) => Notes.Any(p => p.Renote == note); [Projectable] public bool HasReplied(Note note) => Notes.Any(p => p.Reply == note); [Projectable] public bool HasInteractedWith(Note note) => HasLiked(note) || HasReacted(note) || HasBookmarked(note) || HasReplied(note) || HasRenoted(note); public User WithPrecomputedBlockStatus(bool blocking, bool blockedBy) { PrecomputedIsBlocking = blocking; PrecomputedIsBlockedBy = blockedBy; return this; } public User WithPrecomputedMuteStatus(bool muting, bool mutedBy) { PrecomputedIsMuting = muting; PrecomputedIsMutedBy = mutedBy; return this; } public User WithPrecomputedFollowStatus(bool following, bool followedBy, bool requested, bool requestedBy) { PrecomputedIsFollowing = following; PrecomputedIsFollowedBy = followedBy; PrecomputedIsRequested = requested; PrecomputedIsRequestedBy = requestedBy; return this; } public string GetPublicUrl(Config.InstanceSection config) => GetPublicUrl(config.WebDomain); public string GetPublicUri(Config.InstanceSection config) => GetPublicUri(config.WebDomain); public string GetIdenticonUrl(Config.InstanceSection config) => GetIdenticonUrl(config.WebDomain); public string GetPublicUri(string webDomain) => Host == null ? $"https://{webDomain}/users/{Id}" : throw new Exception("Cannot access PublicUri for remote user"); public string GetPublicUrl(string webDomain) => Host == null ? $"https://{webDomain}/@{Username}" : throw new Exception("Cannot access PublicUrl for remote user"); public string GetIdenticonUrl(string webDomain) => $"https://{webDomain}/identicon/{Id}"; }