website/pkg/models/photo.go
Noah de9ba94dd9 Private Photo Sharing
* Add ability to unlock your private photos for others.
    * Link to unlock is on another user's Photos page.
    * You can also access your "Manage Private Photos" page in the User Menu
      or Dashboard and add a user by username.
    * See who you have granted access, and see users who have granted you
      access to their private photos.
* Private photos of users who unlocked them for you may appear on the Site
  Gallery if the photo allows.
* Add new filters to the Site Gallery: you can choose to filter by Explicit,
  limit to a specific Visibility, and order by Newest or Oldest. Non-explicit
  users do not get a filter to see explicit content - they must opt-in in
  their settings.
* Bugfix: Site Gallery was not correctly filtering Explicit photos away from
  users who did not opt-in for explicit!
2022-09-07 21:18:54 -07:00

260 lines
6.5 KiB
Go

package models
import (
"errors"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/log"
"gorm.io/gorm"
)
// Photo table.
type Photo struct {
ID uint64 `gorm:"primaryKey"`
UserID uint64 `gorm:"index"`
Filename string
CroppedFilename string // if cropped, e.g. for profile photo
Filesize int64
Caption string
Flagged bool // photo has been reported by the community
Visibility PhotoVisibility
Gallery bool // photo appears in the public gallery (if public)
Explicit bool // is an explicit photo
CreatedAt time.Time
UpdatedAt time.Time
}
// PhotoVisibility settings.
type PhotoVisibility string
const (
PhotoPublic PhotoVisibility = "public" // on profile page and/or public gallery
PhotoFriends = "friends" // only friends can see it
PhotoPrivate = "private" // private
)
// PhotoVisibility preset settings.
var (
PhotoVisibilityAll = []PhotoVisibility{
PhotoPublic,
PhotoFriends,
PhotoPrivate,
}
// Site Gallery visibility for when your friends show up in the gallery.
// Or: "Friends + Gallery" photos can appear to your friends in the Site Gallery.
PhotoVisibilityFriends = []string{
string(PhotoPublic),
string(PhotoFriends),
}
)
// CreatePhoto with most of the settings you want (not ID or timestamps) in the database.
func CreatePhoto(tmpl Photo) (*Photo, error) {
if tmpl.UserID == 0 {
return nil, errors.New("UserID required")
}
p := &Photo{
UserID: tmpl.UserID,
Filename: tmpl.Filename,
CroppedFilename: tmpl.CroppedFilename,
Caption: tmpl.Caption,
Visibility: tmpl.Visibility,
Gallery: tmpl.Gallery,
Explicit: tmpl.Explicit,
}
result := DB.Create(p)
return p, result.Error
}
// GetPhoto by ID.
func GetPhoto(id uint64) (*Photo, error) {
p := &Photo{}
result := DB.First(&p, id)
return p, result.Error
}
// GetPhotos by an array of IDs, mapped to their IDs.
func GetPhotos(IDs []uint64) (map[uint64]*Photo, error) {
var (
mp = map[uint64]*Photo{}
ps = []*Photo{}
)
result := DB.Model(&Photo{}).Where("id IN ?", IDs).Find(&ps)
for _, row := range ps {
mp[row.ID] = row
}
return mp, result.Error
}
/*
PaginateUserPhotos gets a page of photos belonging to a user ID.
*/
func PaginateUserPhotos(userID uint64, visibility []PhotoVisibility, explicitOK bool, pager *Pagination) ([]*Photo, error) {
var p = []*Photo{}
var explicit = []bool{false}
if explicitOK {
explicit = []bool{true, false}
}
query := DB.Where(
"user_id = ? AND visibility IN ? AND explicit IN ?",
userID,
visibility,
explicit,
).Order(
pager.Sort,
)
// Get the total count.
query.Model(&Photo{}).Count(&pager.Total)
result := query.Offset(
pager.GetOffset(),
).Limit(pager.PerPage).Find(&p)
return p, result.Error
}
// CountPhotos returns the total number of photos on a user's account.
func CountPhotos(userID uint64) int64 {
var count int64
result := DB.Where(
"user_id = ?",
userID,
).Model(&Photo{}).Count(&count)
if result.Error != nil {
log.Error("CountPhotos(%d): %s", userID, result.Error)
}
return count
}
// CountExplicitPhotos returns the number of explicit photos a user has (so non-explicit viewers can see some do exist)
func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, error) {
query := DB.Where(
"user_id = ? AND visibility IN ? AND explicit = ?",
userID,
visibility,
true,
)
var count int64
result := query.Model(&Photo{}).Count(&count)
return count, result.Error
}
/*
PaginateGalleryPhotos gets a page of all public user photos for the site gallery.
Admin view returns ALL photos regardless of Gallery status.
*/
func PaginateGalleryPhotos(user *User, filterExplicit, filterVisibility string, pager *Pagination) ([]*Photo, error) {
var (
p = []*Photo{}
query *gorm.DB
// Get the user ID and their preferences.
userID = user.ID
adminView = user.IsAdmin // Admins see everything on the site.
explicitOK = user.Explicit // User opted-in for explicit content
blocklist = BlockedUserIDs(userID)
friendIDs = FriendIDs(userID)
privateUserIDs = PrivateGrantedUserIDs(userID)
wheres = []string{}
placeholders = []interface{}{}
)
// Include ourself in our friend IDs.
friendIDs = append(friendIDs, userID)
// You can see friends' Friend photos but only public for non-friends.
wheres = append(wheres,
"((user_id IN ? AND visibility IN ?) OR "+
"(user_id IN ? AND visibility IN ?) OR "+
"(user_id NOT IN ? AND visibility = ?))",
)
placeholders = append(placeholders,
friendIDs, PhotoVisibilityFriends,
privateUserIDs, PhotoVisibilityAll,
friendIDs, PhotoPublic,
)
// Gallery photos only.
wheres = append(wheres, "gallery = ?")
placeholders = append(placeholders, true)
// Filter blocked users.
if len(blocklist) > 0 {
wheres = append(wheres, "user_id NOT IN ?")
placeholders = append(placeholders, blocklist)
}
// Non-explicit pics unless the user opted in. Allow explicit filter setting to override.
if filterExplicit != "" {
wheres = append(wheres, "explicit = ?")
placeholders = append(placeholders, filterExplicit == "true")
} else if !explicitOK {
wheres = append(wheres, "explicit = ?")
placeholders = append(placeholders, false)
}
// Is the user furthermore clamping the visibility filter?
if filterVisibility != "" {
wheres = append(wheres, "visibility = ?")
placeholders = append(placeholders, filterVisibility)
}
// Only certified user photos.
wheres = append(wheres,
"EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true)",
)
// Exclude private users' photos.
wheres = append(wheres,
"NOT EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND visibility = 'private')",
)
// Admin view: get ALL PHOTOS on the site, period.
if adminView {
query = DB
// Admin may filter too.
if filterVisibility != "" {
query = query.Where("visibility = ?", filterVisibility)
}
if filterExplicit != "" {
query = query.Where("explicit = ?", filterExplicit == "true")
}
} else {
query = DB.Where(
strings.Join(wheres, " AND "),
placeholders...,
)
}
query = query.Order(pager.Sort)
query.Model(&Photo{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&p)
return p, result.Error
}
// Save photo.
func (p *Photo) Save() error {
result := DB.Save(p)
return result.Error
}
// Delete photo.
func (p *Photo) Delete() error {
result := DB.Delete(p)
return result.Error
}