website/pkg/models/friend.go
2024-01-10 18:08:17 -08:00

517 lines
14 KiB
Go

package models
import (
"errors"
"strings"
"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"`
Ignored bool
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 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
result := DB.Where(
"target_user_id = ? AND approved = ? AND ignored IS NOT true",
userID,
false,
).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 = ?",
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{}
query *gorm.DB
)
if requests && sent && ignored {
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 = ? AND ignored IS NOT true",
user.ID, false,
)
} else if sent {
query = DB.Where(
"source_user_id = ? AND approved = ? AND ignored IS NOT true",
user.ID, false,
)
} else if ignored {
query = DB.Where(
"target_user_id = ? AND approved = ? AND ignored = ?",
user.ID, false, true,
)
} 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 || 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]
}