517 lines
14 KiB
Go
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]
|
|
}
|