package models import ( "fmt" "time" "code.nonshy.com/nonshy/website/pkg/log" "gorm.io/gorm" ) // Block table. type Block struct { ID uint64 `gorm:"primaryKey"` SourceUserID uint64 `gorm:"index"` TargetUserID uint64 `gorm:"index"` CreatedAt time.Time UpdatedAt time.Time } // AddBlock is sourceUserId adding targetUserId to their block list. func AddBlock(sourceUserID, targetUserID uint64) error { // Unfriend in the process. RemoveFriend(sourceUserID, targetUserID) // Did we already block this user? var b *Block forward := DB.Where( "source_user_id = ? AND target_user_id = ?", sourceUserID, targetUserID, ).First(&b).Error // Update existing. if forward == nil { return nil } // Create the block. b = &Block{ SourceUserID: sourceUserID, TargetUserID: targetUserID, } return DB.Create(b).Error } // IsBlocking quickly sees if either user blocks the other. func IsBlocking(sourceUserID, targetUserID uint64) bool { b := &Block{} result := DB.Where( "(source_user_id = ? AND target_user_id = ?) OR "+ "(target_user_id = ? AND source_user_id = ?)", sourceUserID, targetUserID, sourceUserID, targetUserID, ).First(&b) return result.Error == nil } // IsBlocked quickly checks if sourceUserID currently blocks targetUserID. func IsBlocked(sourceUserID, targetUserID uint64) bool { b := &Block{} result := DB.Where( "source_user_id = ? AND target_user_id = ?", sourceUserID, targetUserID, ).First(&b) return result.Error == nil } // PaginateBlockList views a user's blocklist. func PaginateBlockList(user *User, pager *Pagination) ([]*User, error) { // We paginate over the Block table. var ( bs = []*Block{} userIDs = []uint64{} query *gorm.DB ) query = DB.Where( "source_user_id = ?", user.ID, ) query = query.Order(pager.Sort) query.Model(&Block{}).Count(&pager.Total) result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&bs) if result.Error != nil { return nil, result.Error } // Now of these friends get their User objects. for _, b := range bs { userIDs = append(userIDs, b.TargetUserID) } return GetUsers(user, userIDs) } // BlockedUserIDs returns all user IDs blocked by the user (bidirectional, source or target user). func BlockedUserIDs(user *User) []uint64 { // Have we looked this up already on this request? if user.cacheBlockedUserIDs != nil { return user.cacheBlockedUserIDs } var ( bs = []*Block{} userIDs = []uint64{} ) DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Find(&bs) for _, row := range bs { for _, uid := range []uint64{row.TargetUserID, row.SourceUserID} { if uid != user.ID { userIDs = append(userIDs, uid) } } } // Cache the result in the User so we don't query it again. user.cacheBlockedUserIDs = userIDs return userIDs } // MapBlockedUserIDs returns BlockedUserIDs as a lookup hashmap (not for front-end templates currently). func MapBlockedUserIDs(user *User) map[uint64]interface{} { var ( result = map[uint64]interface{}{} blockedIDs = BlockedUserIDs(user) ) for _, uid := range blockedIDs { result[uid] = nil } return result } // FilterBlockingUserIDs narrows down a set of User IDs to remove ones that block (or are blocked by) the current user. func FilterBlockingUserIDs(currentUser *User, userIDs []uint64) []uint64 { var ( // Get the IDs to exclude. blockedIDs = MapBlockedUserIDs(currentUser) // Filter the result. result = []uint64{} ) for _, uid := range userIDs { if _, ok := blockedIDs[uid]; ok { continue } result = append(result, uid) } return result } // BlockedUserIDsByUser returns all user IDs blocked by the user (one directional only) func BlockedUserIDsByUser(userId uint64) []uint64 { var ( bs = []*Block{} userIDs = []uint64{} ) DB.Where("source_user_id = ?", userId).Find(&bs) for _, row := range bs { for _, uid := range []uint64{row.TargetUserID, row.SourceUserID} { if uid != userId { userIDs = append(userIDs, uid) } } } return userIDs } // GetAllBlockedUserIDs returns the forward and reverse lists of blocked user IDs for the user. func GetAllBlockedUserIDs(user *User) (forward, reverse []uint64) { var ( bs = []*Block{} ) DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Find(&bs) for _, row := range bs { if row.SourceUserID == user.ID { forward = append(forward, row.TargetUserID) } else if row.TargetUserID == user.ID { reverse = append(reverse, row.SourceUserID) } } return forward, reverse } // BulkRestoreBlockedUserIDs inserts many blocked user IDs in one query. // // Returns the count of blocks added. func BulkRestoreBlockedUserIDs(user *User, forward, reverse []uint64) (int, error) { var bs = []*Block{} // Forward list. for _, uid := range forward { bs = append(bs, &Block{ SourceUserID: user.ID, TargetUserID: uid, }) } // Reverse list. for _, uid := range reverse { bs = append(bs, &Block{ SourceUserID: uid, TargetUserID: user.ID, }) } // Anything to do? if len(bs) == 0 { return 0, nil } // Batch create. res := DB.Create(bs) return len(bs), res.Error } // BlockedUsernames returns all usernames blocked by (or blocking) the user. func BlockedUsernames(user *User) []string { var ( userIDs = BlockedUserIDs(user) usernames = []string{} ) if len(userIDs) == 0 { return usernames } if res := DB.Table( "users", ).Select( "username", ).Where( "id IN ?", userIDs, ).Scan(&usernames); res.Error != nil { log.Error("BlockedUsernames(%s): %s", user.Username, res.Error) } return usernames } // GetBlocklistInsights returns detailed block lists (both directions) about a user, for admin insight. func GetBlocklistInsights(user *User) (*BlocklistInsight, error) { // Collect ALL user IDs (both directions) of this user's blocklist. var ( bs = []*Block{} forward = []*Block{} // Users they block reverse = []*Block{} // Users who block the target userIDs = []uint64{user.ID} usernames = map[uint64]string{} admins = map[uint64]bool{} ) // Get the complete blocklist and bucket them into forward and reverse. DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Order("created_at desc").Find(&bs) for _, row := range bs { if row.SourceUserID == user.ID { forward = append(forward, row) userIDs = append(userIDs, row.TargetUserID) } else { reverse = append(reverse, row) userIDs = append(userIDs, row.SourceUserID) } } // Map all the user IDs to user names. if len(userIDs) > 0 { type scanItem struct { ID uint64 Username string IsAdmin bool } var scan = []scanItem{} if res := DB.Table( "users", ).Select( "id", "username", "is_admin", ).Where( "id IN ?", userIDs, ).Scan(&scan); res.Error != nil { return nil, fmt.Errorf("GetBlocklistInsights(%s): mapping user IDs to names: %s", user.Username, res.Error) } for _, row := range scan { usernames[row.ID] = row.Username admins[row.ID] = row.IsAdmin } } // Assemble the final result. var result = &BlocklistInsight{ Blocks: []BlocklistInsightUser{}, BlockedBy: []BlocklistInsightUser{}, } for _, row := range forward { if username, ok := usernames[row.TargetUserID]; ok { result.Blocks = append(result.Blocks, BlocklistInsightUser{ Username: username, IsAdmin: admins[row.TargetUserID], Date: row.CreatedAt, }) } } for _, row := range reverse { if username, ok := usernames[row.SourceUserID]; ok { result.BlockedBy = append(result.BlockedBy, BlocklistInsightUser{ Username: username, IsAdmin: admins[row.SourceUserID], Date: row.CreatedAt, }) } } return result, nil } type BlocklistInsight struct { Blocks []BlocklistInsightUser BlockedBy []BlocklistInsightUser } type BlocklistInsightUser struct { Username string IsAdmin bool Date time.Time } // UnblockUser removes targetUserID from your blocklist. func UnblockUser(sourceUserID, targetUserID uint64) error { result := DB.Where( "source_user_id = ? AND target_user_id = ?", sourceUserID, targetUserID, ).Delete(&Block{}) return result.Error } // Save photo. func (b *Block) Save() error { result := DB.Save(b) return result.Error }