package models import ( "errors" "time" "code.nonshy.com/nonshy/website/pkg/log" "gorm.io/gorm" ) // Friend table. type Friend struct { ID uint64 `gorm:"primaryKey"` SourceUserID uint64 `gorm:"index"` TargetUserID uint64 `gorm:"index"` Approved bool `gorm:"index"` CreatedAt time.Time UpdatedAt time.Time } // AddFriend sends a friend request or accepts one if there was already a pending one. func AddFriend(sourceUserID, targetUserID uint64) error { // Did we already send a friend request? f := &Friend{} forward := DB.Where( "source_user_id = ? AND target_user_id = ?", sourceUserID, targetUserID, ).First(&f).Error // Is there a reverse friend request pending? rev := &Friend{} reverse := DB.Where( "source_user_id = ? AND target_user_id = ?", targetUserID, sourceUserID, ).First(&rev).Error // If the reverse exists (requested us) but not the forward, this completes the friendship. if reverse == nil && forward != nil { // Approve the reverse. rev.Approved = true rev.Save() // Add the matching forward. f = &Friend{ SourceUserID: sourceUserID, TargetUserID: targetUserID, Approved: true, } return DB.Create(f).Error } // If the forward already existed, error. if forward == nil { if f.Approved { return errors.New("you are already friends") } return errors.New("a friend request had already been sent") } // Create the pending forward request. f = &Friend{ SourceUserID: sourceUserID, TargetUserID: targetUserID, Approved: false, } return DB.Create(f).Error } // AreFriends quickly checks if two user IDs are friends. func AreFriends(sourceUserID, targetUserID uint64) bool { f := &Friend{} DB.Where( "source_user_id = ? AND target_user_id = ?", sourceUserID, targetUserID, ).First(&f) return f.Approved } // FriendStatus returns an indicator of friendship status: "none", "pending", "approved" func FriendStatus(sourceUserID, targetUserID uint64) string { f := &Friend{} result := DB.Where( "source_user_id = ? AND target_user_id = ?", sourceUserID, targetUserID, ).First(&f) if result.Error == nil { if f.Approved { return "approved" } return "pending" } return "none" } // FriendIDs returns all user IDs with approved friendship to the user. func FriendIDs(userId uint64) []uint64 { var ( fs = []*Friend{} userIDs = []uint64{} ) DB.Where("source_user_id = ? AND approved = ?", userId, true).Find(&fs) for _, row := range fs { userIDs = append(userIDs, row.TargetUserID) } return userIDs } // FilterFriendIDs can filter down a listing of user IDs and return only the ones who are your friends. func FilterFriendIDs(userIDs []uint64, friendIDs []uint64) []uint64 { var ( seen = map[uint64]interface{}{} filtered = []uint64{} ) // Map the friend IDs out. for _, friendID := range friendIDs { seen[friendID] = nil } // Filter the userIDs. for _, userID := range userIDs { if _, ok := seen[userID]; ok { filtered = append(filtered, userID) } } return filtered } // FilterFriendUsernames takes a list of usernames and returns only the ones who are your friends. func FilterFriendUsernames(currentUser *User, usernames []string) []string { var ( fs = []*Friend{} userIDs = []uint64{} userMap = map[uint64]string{} result = []string{} ) // Map usernames to user IDs. users, err := GetUsersByUsernames(currentUser, usernames) if err != nil { log.Error("FilterFriendUsernames: GetUsersByUsernames: %s", err) return result } for _, user := range users { userIDs = append(userIDs, user.ID) userMap[user.ID] = user.Username } if len(userIDs) == 0 { return result } DB.Where("source_user_id = ? AND approved = ? AND target_user_id IN ?", currentUser.ID, true, userIDs).Find(&fs) for _, row := range fs { result = append(result, userMap[row.TargetUserID]) } return result } // FriendIDsAreExplicit returns friend IDs who have opted-in for Explicit content, // e.g. to notify only them when you uploaded a new Explicit photo so that non-explicit // users don't need to see that notification. func FriendIDsAreExplicit(userId uint64) []uint64 { var ( userIDs = []uint64{} ) err := DB.Table( "friends", ).Joins( "JOIN users ON (users.id = friends.target_user_id)", ).Select( "friends.target_user_id AS friend_id", ).Where( "friends.source_user_id = ? AND friends.approved = ? AND users.explicit = ?", userId, true, true, ).Scan(&userIDs) if err.Error != nil { log.Error("SQL error collecting explicit FriendIDs for %d: %s", userId, err.Error) } return userIDs } // FriendIDsInCircle returns friend IDs who are part of the inner circle. func FriendIDsInCircle(userId uint64) []uint64 { var ( userIDs = []uint64{} ) err := DB.Table( "friends", ).Joins( "JOIN users ON (users.id = friends.target_user_id)", ).Select( "friends.target_user_id AS friend_id", ).Where( "friends.source_user_id = ? AND friends.approved = ? AND (users.inner_circle = ? OR users.is_admin = ?)", userId, true, true, true, ).Scan(&userIDs) if err.Error != nil { log.Error("SQL error collecting circle FriendIDs for %d: %s", userId, err.Error) } return userIDs } // FriendIDsInCircleAreExplicit returns the combined friend IDs who are in the inner circle + have opted in to explicit content. // It is the combination of FriendIDsAreExplicit and FriendIDsInCircle. func FriendIDsInCircleAreExplicit(userId uint64) []uint64 { var ( userIDs = []uint64{} ) err := DB.Table( "friends", ).Joins( "JOIN users ON (users.id = friends.target_user_id)", ).Select( "friends.target_user_id AS friend_id", ).Where( "friends.source_user_id = ? AND friends.approved = ? AND users.explicit = ? AND (users.inner_circle = ? OR users.is_admin = ?)", userId, true, true, true, true, ).Scan(&userIDs) if err.Error != nil { log.Error("SQL error collecting explicit FriendIDs for %d: %s", userId, err.Error) } return userIDs } // CountFriendRequests gets a count of pending requests for the user. func CountFriendRequests(userID uint64) (int64, error) { var count int64 result := DB.Where( "target_user_id = ? AND approved = ?", userID, false, ).Model(&Friend{}).Count(&count) return count, result.Error } /* PaginateFriends gets a page of friends (or pending friend requests) as User objects ordered by friendship date. The `requests` and `sent` bools are mutually exclusive (use only one, or neither). `requests` asks for unanswered friend requests to you, and `sent` returns the friend requests that you have sent and have not been answered. */ func PaginateFriends(user *User, requests bool, sent bool, pager *Pagination) ([]*User, error) { // We paginate over the Friend table. var ( fs = []*Friend{} userIDs = []uint64{} query *gorm.DB ) if requests && sent { return nil, errors.New("requests and sent are mutually exclusive options, use one or neither") } if requests { query = DB.Where( "target_user_id = ? AND approved = ?", user.ID, false, ) } else if sent { query = DB.Where( "source_user_id = ? AND approved = ?", user.ID, false, ) } else { query = DB.Where( "source_user_id = ? AND approved = ?", user.ID, true, ) } query = query.Order(pager.Sort) query.Model(&Friend{}).Count(&pager.Total) result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs) if result.Error != nil { return nil, result.Error } // Now of these friends get their User objects. for _, friend := range fs { if requests { userIDs = append(userIDs, friend.SourceUserID) } else { userIDs = append(userIDs, friend.TargetUserID) } } return GetUsers(user, userIDs) } // GetFriendRequests returns all pending friend requests for a user. func GetFriendRequests(userID uint64) ([]*Friend, error) { var fs = []*Friend{} result := DB.Where( "target_user_id = ? AND approved = ?", userID, false, ).Find(&fs) return fs, result.Error } // RemoveFriend severs a friend connection both directions, used when // rejecting a request or removing a friend. func RemoveFriend(sourceUserID, targetUserID uint64) error { result := DB.Where( "(source_user_id = ? AND target_user_id = ?) OR "+ "(target_user_id = ? AND source_user_id = ?)", sourceUserID, targetUserID, sourceUserID, targetUserID, ).Delete(&Friend{}) return result.Error } // Save photo. func (f *Friend) Save() error { result := DB.Save(f) return result.Error } // FriendMap maps user IDs to friendship status for the current user. type FriendMap map[uint64]bool // MapFriends looks up a set of user IDs in bulk and returns a FriendMap suitable for templates. func MapFriends(currentUser *User, users []*User) FriendMap { var ( usermap = FriendMap{} set = map[uint64]interface{}{} distinct = []uint64{} ) // Uniqueify users. for _, user := range users { if _, ok := set[user.ID]; ok { continue } set[user.ID] = nil distinct = append(distinct, user.ID) } var ( matched = []*Friend{} result = DB.Model(&Friend{}).Where( "source_user_id = ? AND target_user_id IN ? AND approved = ?", currentUser.ID, distinct, true, ).Find(&matched) ) if result.Error == nil { for _, row := range matched { usermap[row.TargetUserID] = true } } return usermap } // Get a user from the FriendMap. func (um FriendMap) Get(id uint64) bool { return um[id] }