2022-08-14 05:44:57 +00:00
package models
import (
"errors"
2023-10-22 22:02:24 +00:00
"strings"
2022-08-14 05:44:57 +00:00
"time"
2023-05-07 20:16:22 +00:00
"code.nonshy.com/nonshy/website/pkg/log"
2022-08-14 05:44:57 +00:00
"gorm.io/gorm"
)
// Friend table.
type Friend struct {
ID uint64 ` gorm:"primaryKey" `
SourceUserID uint64 ` gorm:"index" `
TargetUserID uint64 ` gorm:"index" `
Approved bool ` gorm:"index" `
2023-10-23 02:57:18 +00:00
Ignored bool
2022-08-14 05:44:57 +00:00
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
2023-11-25 21:39:38 +00:00
// 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" ,
)
}
2022-08-14 05:44:57 +00:00
// 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
2023-10-23 02:57:18 +00:00
rev . Ignored = false
2022-08-14 05:44:57 +00:00
rev . Save ( )
// Add the matching forward.
f = & Friend {
SourceUserID : sourceUserID ,
TargetUserID : targetUserID ,
Approved : true ,
2023-10-23 02:57:18 +00:00
Ignored : false ,
2022-08-14 05:44:57 +00:00
}
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"
}
2022-08-23 03:58:35 +00:00
// 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
}
2023-08-15 01:50:34 +00:00
// 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
}
2023-10-08 20:35:11 +00:00
// 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
}
2023-05-07 20:16:22 +00:00
// 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 {
2023-07-30 17:33:04 +00:00
log . Error ( "SQL error collecting explicit FriendIDs for %d: %s" , userId , err . Error )
2023-05-07 20:16:22 +00:00
}
return userIDs
}
2023-05-24 03:04:17 +00:00
// 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 {
2023-08-02 03:39:48 +00:00
log . Error ( "SQL error collecting circle FriendIDs for %d: %s" , userId , err . Error )
2023-05-24 03:04:17 +00:00
}
return userIDs
}
2023-05-24 18:27:42 +00:00
// 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 {
2023-08-02 03:39:48 +00:00
log . Error ( "SQL error collecting explicit FriendIDs for %d: %s" , userId , err . Error )
2023-05-24 18:27:42 +00:00
}
return userIDs
}
2022-08-14 05:44:57 +00:00
// CountFriendRequests gets a count of pending requests for the user.
func CountFriendRequests ( userID uint64 ) ( int64 , error ) {
var count int64
result := DB . Where (
2023-10-23 02:57:18 +00:00
"target_user_id = ? AND approved = ? AND ignored IS NOT true" ,
2022-08-14 05:44:57 +00:00
userID ,
false ,
) . Model ( & Friend { } ) . Count ( & count )
return count , result . Error
}
2023-10-23 02:57:18 +00:00
// 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 = ?" ,
userID ,
false ,
true ,
) . Model ( & Friend { } ) . Count ( & count )
return count , result . Error
}
2023-10-22 22:02:24 +00:00
// 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
}
2022-08-23 03:58:35 +00:00
/ *
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 .
* /
2023-10-23 02:57:18 +00:00
func PaginateFriends ( user * User , requests bool , sent bool , ignored bool , pager * Pagination ) ( [ ] * User , error ) {
2022-08-14 05:44:57 +00:00
// We paginate over the Friend table.
var (
fs = [ ] * Friend { }
userIDs = [ ] uint64 { }
query * gorm . DB
)
2023-10-23 02:57:18 +00:00
if requests && sent && ignored {
2022-08-23 03:58:35 +00:00
return nil , errors . New ( "requests and sent are mutually exclusive options, use one or neither" )
}
2022-08-14 05:44:57 +00:00
if requests {
query = DB . Where (
2023-10-23 02:57:18 +00:00
"target_user_id = ? AND approved = ? AND ignored IS NOT true" ,
2022-09-09 04:42:20 +00:00
user . ID , false ,
2022-08-23 03:58:35 +00:00
)
} else if sent {
query = DB . Where (
2023-10-23 02:57:18 +00:00
"source_user_id = ? AND approved = ? AND ignored IS NOT true" ,
2022-09-09 04:42:20 +00:00
user . ID , false ,
2022-08-14 05:44:57 +00:00
)
2023-10-23 02:57:18 +00:00
} else if ignored {
query = DB . Where (
"target_user_id = ? AND approved = ? AND ignored = ?" ,
user . ID , false , true ,
)
2022-08-14 05:44:57 +00:00
} else {
query = DB . Where (
"source_user_id = ? AND approved = ?" ,
2022-09-09 04:42:20 +00:00
user . ID , true ,
2022-08-14 05:44:57 +00:00
)
}
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 {
2023-10-23 02:57:18 +00:00
if requests || ignored {
2022-08-14 05:44:57 +00:00
userIDs = append ( userIDs , friend . SourceUserID )
} else {
userIDs = append ( userIDs , friend . TargetUserID )
}
}
2022-09-09 04:42:20 +00:00
return GetUsers ( user , userIDs )
2022-08-14 05:44:57 +00:00
}
2023-10-22 22:02:24 +00:00
/ *
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 )
}
2023-10-23 07:04:25 +00:00
return GetUsers ( currentUser , userIDs )
2023-10-22 22:02:24 +00:00
}
2022-08-14 05:44:57 +00:00
// 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
}
2023-10-23 02:57:18 +00:00
// 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" )
}
2022-08-14 05:44:57 +00:00
// 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
}
2024-01-11 04:47:38 +00:00
// 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.
2024-01-11 06:25:50 +00:00
//
// 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 )
2024-01-11 04:47:38 +00:00
if err != nil {
return err
2024-01-11 06:25:50 +00:00
} else if len ( allPhotoIDs ) == 0 {
2024-01-11 04:47:38 +00:00
// Nothing to do.
return nil
}
2024-01-11 06:25:50 +00:00
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 )
2024-01-11 04:47:38 +00:00
}
2022-08-14 05:44:57 +00:00
// Save photo.
func ( f * Friend ) Save ( ) error {
result := DB . Save ( f )
return result . Error
}
2023-09-02 00:12:27 +00:00
// 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 ]
}