diff --git a/pkg/controller/account/settings.go b/pkg/controller/account/settings.go index a6a77fe..34fa087 100644 --- a/pkg/controller/account/settings.go +++ b/pkg/controller/account/settings.go @@ -224,6 +224,7 @@ func Settings() http.HandlerFunc { var ( visibility = models.UserVisibility(r.PostFormValue("visibility")) dmPrivacy = r.PostFormValue("dm_privacy") + ppPrivacy = r.PostFormValue("private_photo_gate") ) user.Visibility = models.UserVisibilityPublic @@ -236,6 +237,7 @@ func Settings() http.HandlerFunc { // Set profile field prefs. user.SetProfileField("dm_privacy", dmPrivacy) + user.SetProfileField("private_photo_gate", ppPrivacy) if err := user.Save(); err != nil { session.FlashError(w, r, "Failed to save user to database: %s", err) diff --git a/pkg/controller/photo/private.go b/pkg/controller/photo/private.go index f057ed0..428fa26 100644 --- a/pkg/controller/photo/private.go +++ b/pkg/controller/photo/private.go @@ -42,6 +42,12 @@ func Private() http.HandlerFunc { return } + // Collect user IDs for some mappings. + var userIDs = []uint64{} + for _, user := range users { + userIDs = append(userIDs, user.ID) + } + // Map reverse grantee statuses. var GranteeMap interface{} if isGrantee { @@ -58,6 +64,12 @@ func Private() http.HandlerFunc { "GranteeMap": GranteeMap, "Users": users, "Pager": pager, + + // Mapped user statuses for frontend cards. + "PhotoCountMap": models.MapPhotoCountsByVisibility(users, models.PhotoPrivate), + "FriendMap": models.MapFriends(currentUser, users), + "LikedMap": models.MapLikes(currentUser, "users", userIDs), + "ShyMap": models.MapShyAccounts(users), } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -129,6 +141,15 @@ func Share() http.HandlerFunc { intent = r.PostFormValue("intent") ) + // Is the recipient blocking this photo share? + if intent != "decline" && intent != "revoke" { + if ok, err := models.ShouldShowPrivateUnlockPrompt(currentUser, user); !ok { + session.FlashError(w, r, "You are unable to share your private photos with %s: %s.", user.Username, err) + templates.Redirect(w, "/u/"+user.Username) + return + } + } + // If submitting, do it and redirect. if intent == "submit" { models.UnlockPrivatePhotos(currentUser.ID, user.ID) @@ -164,6 +185,21 @@ func Share() http.HandlerFunc { log.Error("RevokePrivatePhotoNotifications(%s): %s", currentUser.Username, err) } return + } else if intent == "decline" { + // Decline = they shared with me and we do not want it. + models.RevokePrivatePhotos(user.ID, currentUser.ID) + session.Flash(w, r, "You have declined access to see %s's private photos.", user.Username) + + // Remove any notification we created when the grant was given. + models.RemoveSpecificNotification(currentUser.ID, models.NotificationPrivatePhoto, "__private_photos", user.ID) + + // Revoke any "has uploaded a new private photo" notifications in this user's list. + if err := models.RevokePrivatePhotoNotifications(user, currentUser); err != nil { + log.Error("RevokePrivatePhotoNotifications(%s): %s", user.Username, err) + } + + templates.Redirect(w, "/photo/private?view=grantee") + return } // The other intent is "preview" so the user gets the confirmation diff --git a/pkg/controller/photo/user_gallery.go b/pkg/controller/photo/user_gallery.go index 5e1d39b..d1fbcba 100644 --- a/pkg/controller/photo/user_gallery.go +++ b/pkg/controller/photo/user_gallery.go @@ -195,12 +195,16 @@ func UserPhotos() http.HandlerFunc { areNotificationsMuted = !v } + // Should the current user be able to share their private photos with the target? + showPrivateUnlockPrompt, _ := models.ShouldShowPrivateUnlockPrompt(currentUser, user) + var vars = map[string]interface{}{ "IsOwnPhotos": currentUser.ID == user.ID, "IsShyUser": isShy, "IsShyFrom": isShyFrom, "IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics? "AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access. + "ShowPrivateUnlockPrompt": showPrivateUnlockPrompt, "AreFriends": areFriends, "AreNotificationsMuted": areNotificationsMuted, "ProfilePictureHiddenVisibility": profilePictureHidden, diff --git a/pkg/models/message.go b/pkg/models/message.go index 19624e8..7cd3792 100644 --- a/pkg/models/message.go +++ b/pkg/models/message.go @@ -169,6 +169,16 @@ func HasMessageThread(a, b *User) (uint64, bool) { return 0, false } +// HasSentAMessage tells if the source user has sent a DM to the target user. +func HasSentAMessage(sourceUser, targetUser *User) bool { + var count int64 + DB.Model(&Message{}).Where( + "source_user_id = ? AND target_user_id = ?", + sourceUser.ID, targetUser.ID, + ).Count(&count) + return count > 0 +} + // DeleteMessageThread removes all message history between two people. func DeleteMessageThread(message *Message) error { return DB.Where( diff --git a/pkg/models/photo.go b/pkg/models/photo.go index 569662e..be7d8c3 100644 --- a/pkg/models/photo.go +++ b/pkg/models/photo.go @@ -451,6 +451,11 @@ func CountPhotosICanSee(user *User, viewer *User) int64 { // MapPhotoCounts returns a mapping of user ID to the CountPhotos()-equivalent result for each. // It's used on the member directory to show photo counts on each user card. func MapPhotoCounts(users []*User) PhotoCountMap { + return MapPhotoCountsByVisibility(users, PhotoPublic) +} + +// MapPhotoCountsByVisibility returns a mapping of user ID to the CountPhotos()-equivalent result for each. +func MapPhotoCountsByVisibility(users []*User, visibility PhotoVisibility) PhotoCountMap { var ( userIDs = []uint64{} result = PhotoCountMap{} @@ -471,7 +476,7 @@ func MapPhotoCounts(users []*User) PhotoCountMap { ).Select( "user_id, count(id) AS photo_count", ).Where( - "user_id IN ? AND visibility = ?", userIDs, PhotoPublic, + "user_id IN ? AND visibility = ?", userIDs, visibility, ).Group("user_id").Scan(&groups); res.Error != nil { log.Error("CountPhotosForUsers: %s", res.Error) } @@ -590,16 +595,21 @@ func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, er // CountPublicPhotos returns the number of public photos on a user's page. func CountPublicPhotos(userID uint64) int64 { + return CountUserPhotosByVisibility(userID, PhotoPublic) +} + +// CountUserPhotosByVisibility returns the number of a user's photos by visibility. +func CountUserPhotosByVisibility(userID uint64, visibility PhotoVisibility) int64 { query := DB.Where( "user_id = ? AND visibility = ?", userID, - PhotoPublic, + visibility, ) var count int64 result := query.Model(&Photo{}).Count(&count) if result.Error != nil { - log.Error("CountPublicPhotos(%d): %s", userID, result.Error) + log.Error("CountUserPhotosByVisibility(%d, %s): %s", userID, visibility, result.Error) } return count } diff --git a/pkg/models/private_photo.go b/pkg/models/private_photo.go index 736ac54..4cb8b15 100644 --- a/pkg/models/private_photo.go +++ b/pkg/models/private_photo.go @@ -1,6 +1,7 @@ package models import ( + "errors" "fmt" "strings" "time" @@ -125,6 +126,43 @@ func (u *User) AllPhotoIDs() ([]uint64, error) { 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{} diff --git a/web/templates/account/settings.html b/web/templates/account/settings.html index b722943..3fcd4af 100644 --- a/web/templates/account/settings.html +++ b/web/templates/account/settings.html @@ -835,7 +835,7 @@ logged-out browser, they are prompted to log in.

-