website/pkg/models/private_photo.go
Noah Petherbridge 1b3e8cb250 Private Photo Sharing Improvements
* Add a user privacy setting so they can gate who is allowed to share private
  photos with them (for people who dislike unsolicited shares):
  * Anybody (default)
  * Friends only
  * Friends + people whom they have sent a DM to (on the main website)
  * Nobody
* Add gating around whether to display the prompt to unlock your private photos
  while you are viewing somebody's gallery:
  * The current user needs at least one private photo to share.
  * The target user's new privacy preference is taken into consideration.
* The "should show private photo share prompt" logic is also used on the actual
  share page, e.g. for people who manually paste in a username to share with.
  You can not grant access to private photos which don't exist.
* Improve the UI on the private photo shares page.
  * Profile cards to add elements from the Member Directory page, such as a
    Friends and Liked indicator.
  * A count of the user's Private photos is shown, which links directly to
    their private gallery.
* Add "Decline" buttons to the Shared With Me page: so the target of a private
  photo share is able to remove/cancel shares with them.
2024-10-19 12:44:47 -07:00

416 lines
11 KiB
Go

package models
import (
"errors"
"fmt"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/log"
"gorm.io/gorm"
)
// PrivatePhoto table to track who you have unlocked your private photos for.
type PrivatePhoto struct {
ID uint64 `gorm:"primaryKey"`
SourceUserID uint64 `gorm:"index"` // the owner of a photo
TargetUserID uint64 `gorm:"index"` // the receiver
CreatedAt time.Time
UpdatedAt time.Time
}
// UnlockPrivatePhotos is sourceUserId allowing targetUserId to see their private photos.
func UnlockPrivatePhotos(sourceUserID, targetUserID uint64) error {
// Did we already allow this user?
var pb *PrivatePhoto
exist := DB.Where(
"source_user_id = ? AND target_user_id = ?",
sourceUserID, targetUserID,
).First(&pb).Error
// Update existing.
if exist == nil {
return nil
}
// Create the PrivatePhoto.
pb = &PrivatePhoto{
SourceUserID: sourceUserID,
TargetUserID: targetUserID,
}
return DB.Create(pb).Error
}
// RevokePrivatePhotos is sourceUserId revoking targetUserId to see their private photos.
func RevokePrivatePhotos(sourceUserID, targetUserID uint64) error {
result := DB.Where(
"source_user_id = ? AND target_user_id = ?",
sourceUserID, targetUserID,
).Delete(&PrivatePhoto{})
return result.Error
}
// RevokePrivatePhotosAll is sourceUserId revoking ALL USERS from their private photos.
func RevokePrivatePhotosAll(sourceUserID uint64) error {
result := DB.Where(
"source_user_id = ?",
sourceUserID,
).Delete(&PrivatePhoto{})
return result.Error
}
// RevokePrivatePhotoNotifications removes notifications about newly uploaded private photos
// that were sent to one (or multiple) members when the user revokes their access later. Pass
// a nil fromUserID to revoke the photo upload notifications from ALL users.
func RevokePrivatePhotoNotifications(currentUser, fromUser *User) error {
// Gather the IDs of all our private photos to nuke notifications for.
photoIDs, err := currentUser.AllPrivatePhotoIDs()
if err != nil {
return err
} else if len(photoIDs) == 0 {
// Nothing to do.
return nil
}
// Who to clear the notifications for?
if fromUser == nil {
log.Info("RevokePrivatePhotoNotifications(%s): forget about private photo uploads for EVERYBODY on photo IDs: %v", currentUser.Username, photoIDs)
return RemoveNotificationBulk("photos", photoIDs)
} else {
log.Info("RevokePrivatePhotoNotifications(%s): forget about private photo uploads for user %s on photo IDs: %v", currentUser.Username, fromUser.Username, photoIDs)
return RemoveSpecificNotificationBulk([]*User{currentUser, fromUser}, NotificationNewPhoto, "photos", photoIDs)
}
}
// AllPrivatePhotoIDs returns the listing of all IDs of the user's private photos.
func (u *User) AllPrivatePhotoIDs() ([]uint64, error) {
var photoIDs = []uint64{}
err := DB.Table(
"photos",
).Select(
"photos.id AS id",
).Where(
"user_id = ? AND visibility = ?",
u.ID, PhotoPrivate,
).Scan(&photoIDs)
if err.Error != nil {
return photoIDs, fmt.Errorf("AllPrivatePhotoIDs(%s): %s", u.Username, err.Error)
}
return photoIDs, nil
}
// AllPhotoIDs returns the listing of all IDs of the user's photos.
func (u *User) AllPhotoIDs() ([]uint64, error) {
if u.cachePhotoIDs != nil {
return u.cachePhotoIDs, nil
}
var photoIDs = []uint64{}
err := DB.Table(
"photos",
).Select(
"photos.id AS id",
).Where(
"user_id = ?",
u.ID,
).Scan(&photoIDs)
if err.Error != nil {
return photoIDs, fmt.Errorf("AllPhotoIDs(%s): %s", u.Username, err.Error)
}
u.cachePhotoIDs = photoIDs
return photoIDs, nil
}
/*
ShouldShowPrivateUnlockPrompt determines whether the current user should be shown a prompt, when viewing
the other user's gallery, to unlock their private photos for that user.
This function verifies that the source user actually has a private photo to share, and that the target
user doesn't have a privacy setting enabled that should block the private photo unlock request.
*/
func ShouldShowPrivateUnlockPrompt(sourceUser, targetUser *User) (bool, error) {
// If the current user doesn't even have a private photo to share, no prompt.
if CountUserPhotosByVisibility(sourceUser.ID, PhotoPrivate) == 0 {
return false, errors.New("you do not currently have a private photo on your gallery to share")
}
// Does the target user have a privacy setting enabled?
if pp := targetUser.GetProfileField("private_photo_gate"); pp != "" {
areFriends := AreFriends(sourceUser.ID, targetUser.ID)
switch pp {
case "nobody":
return false, errors.New("they decline all private photo sharing")
case "friends":
if areFriends {
return true, nil
}
return false, errors.New("they are only accepting private photos from their friends")
case "messaged":
if areFriends || HasSentAMessage(targetUser, sourceUser) {
return true, nil
}
return false, errors.New("they are only accepting private photos from their friends or from people they have sent a DM to")
}
}
return true, nil
}
// IsPrivateUnlocked quickly sees if sourceUserID has unlocked private photos for targetUserID to see.
func IsPrivateUnlocked(sourceUserID, targetUserID uint64) bool {
pb := &PrivatePhoto{}
result := DB.Where(
"source_user_id = ? AND target_user_id = ?",
sourceUserID, targetUserID,
).First(&pb)
return result.Error == nil
}
// CountPrivateGrantee returns how many users have granted you access to their private photos.
func CountPrivateGrantee(userID uint64) int64 {
var count int64
DB.Model(&PrivatePhoto{}).Where(
"target_user_id = ?",
userID,
).Count(&count)
return count
}
// PrivateGrantedUserIDs returns all user IDs who have granted access for userId to see their private photos.
func PrivateGrantedUserIDs(userId uint64) []uint64 {
var (
ps = []*PrivatePhoto{}
userIDs = []uint64{userId}
)
DB.Where("target_user_id = ?", userId).Find(&ps)
for _, row := range ps {
userIDs = append(userIDs, row.SourceUserID)
}
return userIDs
}
// PrivateGrantedUserIDsAreFriends returns user IDs who have granted us their private pictures, and are also our friends.
func PrivateGrantedUserIDsAreFriends(currentUser *User) []uint64 {
var (
ps = []*PrivatePhoto{}
userIDs = []uint64{}
)
DB.Model(&PrivatePhoto{}).Joins(
"JOIN friends ON friends.source_user_id = private_photos.source_user_id AND friends.target_user_id = private_photos.target_user_id",
).Where(
"private_photos.target_user_id = ? AND friends.approved IS true", currentUser.ID,
).Find(&ps)
for _, row := range ps {
userIDs = append(userIDs, row.SourceUserID)
}
return userIDs
}
// PrivateGranteeUserIDs are the users whom WE have granted access to our photos (userId is the photo owners).
func PrivateGranteeUserIDs(userId uint64) []uint64 {
var (
ps = []*PrivatePhoto{}
userIDs = []uint64{}
)
DB.Where("source_user_id = ?", userId).Find(&ps)
for _, row := range ps {
userIDs = append(userIDs, row.TargetUserID)
}
return userIDs
}
// PrivateGranteeAreExplicitUserIDs gets your private photo grantees who have opted-in to see explicit content.
func PrivateGranteeAreExplicitUserIDs(userId uint64) []uint64 {
var (
userIDs = []uint64{}
)
err := DB.Table(
"private_photos",
).Joins(
"JOIN users ON (users.id = private_photos.target_user_id)",
).Select(
"private_photos.target_user_id AS user_id",
).Where(
"source_user_id = ? AND users.explicit IS TRUE",
userId,
).Scan(&userIDs)
if err.Error != nil {
log.Error("PrivateGranteeAreExplicitUserIDs: %s", err.Error)
}
return userIDs
}
/*
PaginatePrivatePhotoList views a user's list of private photo grants.
If grantee is true, it returns the list of users who have granted YOU access to see THEIR
private photos. If grantee is false, it returns the users that YOU have granted access to
see YOUR OWN private photos.
*/
func PaginatePrivatePhotoList(user *User, grantee bool, pager *Pagination) ([]*User, error) {
var (
pbs = []*PrivatePhoto{}
userIDs = []uint64{}
query *gorm.DB
wheres = []string{}
placeholders = []interface{}{}
blocklist = BlockedUserIDs(user)
// Column name of "other user" depending on direction
otherUserColumn string
)
// Which direction are we going?
if grantee {
// Return the private photo grants for whom YOU are the recipient.
wheres = append(wheres, "target_user_id = ?")
placeholders = append(placeholders, user.ID)
otherUserColumn = "source_user_id"
} else {
// Return the users that YOU have granted access to YOUR private pictures.
wheres = append(wheres, "source_user_id = ?")
placeholders = append(placeholders, user.ID)
otherUserColumn = "target_user_id"
}
// Filter out users who are banned/disabled.
wheres = append(wheres,
fmt.Sprintf(`
EXISTS (
SELECT 1
FROM users
WHERE private_photos.%s = users.id
AND users.status = 'active'
)`,
otherUserColumn,
),
)
// Filter blocked users.
if len(blocklist) > 0 {
wheres = append(wheres, fmt.Sprintf("%s NOT IN ?", otherUserColumn))
placeholders = append(placeholders, blocklist)
}
query = DB.Where(
strings.Join(wheres, " AND "),
placeholders...,
)
query = query.Order(pager.Sort)
query.Model(&PrivatePhoto{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&pbs)
if result.Error != nil {
return nil, result.Error
}
// Now of these user IDs get their User objects.
for _, b := range pbs {
if grantee {
userIDs = append(userIDs, b.SourceUserID)
} else {
userIDs = append(userIDs, b.TargetUserID)
}
}
return GetUsers(user, userIDs)
}
// Save photo.
func (pb *PrivatePhoto) Save() error {
result := DB.Save(pb)
return result.Error
}
// PrivateGranteeMap maps user IDs to whether they have granted you their private photos.
type PrivateGranteeMap map[uint64]bool
// MapPrivatePhotoGrantee looks up a set of user IDs in bulk and returns a PrivateGranteeMap suitable for templates.
func MapPrivatePhotoGrantee(currentUser *User, users []*User) PrivateGranteeMap {
var (
usermap = PrivateGranteeMap{}
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 = []*PrivatePhoto{}
result = DB.Model(&PrivatePhoto{}).
Where("target_user_id = ? AND source_user_id IN ?", currentUser.ID, distinct).
Find(&matched)
)
if result.Error == nil {
for _, row := range matched {
usermap[row.SourceUserID] = true
}
}
return usermap
}
// Get a user from the PrivateGranteeMap.
func (um PrivateGranteeMap) Get(id uint64) bool {
return um[id]
}
// PrivateGrantedMap maps user IDs to whether we have granted our private pictures to them.
type PrivateGrantedMap map[uint64]bool
// MapPrivatePhotoGranted looks up a set of user IDs in bulk and returns a PrivateGrantedMap suitable for templates.
func MapPrivatePhotoGranted(currentUser *User, users []*User) PrivateGrantedMap {
var (
usermap = PrivateGrantedMap{}
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 = []*PrivatePhoto{}
result = DB.Model(&PrivatePhoto{}).
Where("source_user_id = ? AND target_user_id IN ?", currentUser.ID, distinct).
Find(&matched)
)
if result.Error == nil {
for _, row := range matched {
usermap[row.TargetUserID] = true
}
}
return usermap
}
// Get a user from the PrivateGrantedMap.
func (um PrivateGrantedMap) Get(id uint64) bool {
return um[id]
}