package models import ( "errors" "strings" "time" "code.nonshy.com/nonshy/website/pkg/log" ) // Friend table. type Friend struct { ID uint64 `gorm:"primaryKey"` SourceUserID uint64 `gorm:"index"` TargetUserID uint64 `gorm:"index"` Approved bool `gorm:"index"` Ignored bool CreatedAt time.Time UpdatedAt time.Time `gorm:"index"` } // 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 we have previously Ignored the friend request, we can not Accept it but only Reject it. if reverse == nil && rev.Ignored { return errors.New( "you have previously ignored a friend request from this person and can not accept it now - " + "please go to your Friends page, Ignored tab, and remove the ignored friend request there and try again", ) } // 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.Ignored = false rev.Save() // Add the matching forward. f = &Friend{ SourceUserID: sourceUserID, TargetUserID: targetUserID, Approved: true, Ignored: false, } 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 wheres = []string{ "target_user_id = ? AND approved = ? AND ignored IS NOT true", "EXISTS (SELECT 1 FROM users WHERE users.id = source_user_id AND users.status = 'active')", } placeholders = []interface{}{ userID, false, } ) result := DB.Where( strings.Join(wheres, " AND "), placeholders..., ).Model(&Friend{}).Count(&count) return count, result.Error } // CountIgnoredFriendRequests gets a count of ignored pending friend requests for the user. func CountIgnoredFriendRequests(userID uint64) (int64, error) { var count int64 result := DB.Where( "target_user_id = ? AND approved = ? AND ignored = ? AND EXISTS (SELECT 1 FROM users WHERE users.id = friends.source_user_id AND users.status = 'active')", userID, false, true, ).Model(&Friend{}).Count(&count) return count, result.Error } // CountFriends gets a count of friends for the user. func CountFriends(userID uint64) int64 { var count int64 result := DB.Where( "target_user_id = ? AND approved = ? AND EXISTS (SELECT 1 FROM users WHERE users.id = friends.source_user_id AND users.status = 'active')", userID, true, ).Model(&Friend{}).Count(&count) if result.Error != nil { log.Error("CountFriends(%d): %s", userID, result.Error) } return count } /* 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, ignored bool, pager *Pagination) ([]*User, error) { // We paginate over the Friend table. var ( fs = []*Friend{} userIDs = []uint64{} blockedUserIDs = BlockedUserIDs(user) wheres = []string{} placeholders = []interface{}{} query = DB.Model(&Friend{}) ) if requests && sent && ignored { return nil, errors.New("requests and sent are mutually exclusive options, use one or neither") } // Don't show our blocked users in the result. if len(blockedUserIDs) > 0 { wheres = append(wheres, "target_user_id NOT IN ?") placeholders = append(placeholders, blockedUserIDs) } // Don't show disabled or banned users. var ( // Source user is banned (Requests, Ignored tabs) bannedWhereRequest = ` EXISTS ( SELECT 1 FROM users WHERE users.id = friends.source_user_id AND users.status = 'active' ) ` // Target user is banned (Friends, Sent tabs) bannedWhereFriend = ` EXISTS ( SELECT 1 FROM users WHERE users.id = friends.target_user_id AND users.status = 'active' ) ` ) if requests { wheres = append(wheres, "target_user_id = ? AND approved = ? AND ignored IS NOT true") placeholders = append(placeholders, user.ID, false) // Don't show friend requests from currently banned/disabled users. wheres = append(wheres, bannedWhereRequest) } else if sent { wheres = append(wheres, "source_user_id = ? AND approved = ? AND ignored IS NOT true") placeholders = append(placeholders, user.ID, false) // Don't show friends who are currently banned/disabled. wheres = append(wheres, bannedWhereFriend) } else if ignored { wheres = append(wheres, "target_user_id = ? AND approved = ? AND ignored = ?") placeholders = append(placeholders, user.ID, false, true) // Don't show friend requests from currently banned/disabled users. wheres = append(wheres, bannedWhereRequest) } else { wheres = append(wheres, "source_user_id = ? AND approved = ?") placeholders = append(placeholders, user.ID, true) // Don't show friends who are currently banned/disabled. wheres = append(wheres, bannedWhereFriend) } query = query.Where( strings.Join(wheres, " AND "), placeholders..., ).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 || ignored { userIDs = append(userIDs, friend.SourceUserID) } else { userIDs = append(userIDs, friend.TargetUserID) } } return GetUsers(user, userIDs) } /* PaginateOtherUserFriends gets a page of friends from another user, for their profile page. */ func PaginateOtherUserFriends(currentUser *User, user *User, pager *Pagination) ([]*User, error) { // We paginate over the Friend table. var ( fs = []*Friend{} blockedUserIDs = BlockedUserIDs(currentUser) userIDs = []uint64{} wheres = []string{} placeholders = []interface{}{} query = DB.Model(&Friend{}) ) // Get friends of the target user. wheres = append(wheres, "source_user_id = ? AND approved = ?") placeholders = append(placeholders, user.ID, true) // Don't show our blocked users in the result. if len(blockedUserIDs) > 0 { wheres = append(wheres, "target_user_id NOT IN ?") placeholders = append(placeholders, blockedUserIDs) } // Don't show disabled or banned users. wheres = append(wheres, ` EXISTS ( SELECT 1 FROM users WHERE users.id = friends.target_user_id AND users.status = 'active' ) `) query = query.Where( strings.Join(wheres, " AND "), placeholders..., ).Order(pager.Sort) // Get the total count. query.Count(&pager.Total) // Get the page. 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 { userIDs = append(userIDs, friend.TargetUserID) } return GetUsers(currentUser, 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 } // IgnoreFriendRequest ignores a pending friend request that was sent to targetUserID. func IgnoreFriendRequest(currentUser *User, fromUser *User) error { // Is there a reverse friend request pending? (The one we ideally hope to mark Ignored) rev := &Friend{} reverse := DB.Where( "source_user_id = ? AND target_user_id = ?", fromUser.ID, currentUser.ID, ).First(&rev).Error // If the reverse exists (requested us) mark it as Ignored. if reverse == nil { // Ignore the reverse friend request (happy path). log.Error("%s ignoring friend request from %s", currentUser.Username, fromUser.Username) rev.Approved = false rev.Ignored = true return rev.Save() } log.Error("rev: %+v", rev) return errors.New("unexpected error while ignoring friend request") } // 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 } // RevokeFriendPhotoNotifications removes notifications about newly uploaded friends photos // that were sent to your former friends, when you remove their friendship. // // For example: if I unfriend you, all your past notifications that showed my friends-only photos should // be revoked so that you can't see them anymore. // // Notifications about friend photos are revoked going in both directions. func RevokeFriendPhotoNotifications(currentUser, other *User) error { // Gather the IDs of all their friends-only photos to nuke notifications for. allPhotoIDs, err := AllFriendsOnlyPhotoIDs(currentUser, other) if err != nil { return err } else if len(allPhotoIDs) == 0 { // Nothing to do. return nil } log.Info("RevokeFriendPhotoNotifications(%s): forget about friend photo uploads for user %s on photo IDs: %v", currentUser.Username, other.Username, allPhotoIDs) return RemoveSpecificNotificationBulk([]*User{currentUser, other}, NotificationNewPhoto, "photos", allPhotoIDs) } // 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] }