diff --git a/pkg/controller/account/profile.go b/pkg/controller/account/profile.go index f9b9795..efcc69a 100644 --- a/pkg/controller/account/profile.go +++ b/pkg/controller/account/profile.go @@ -104,7 +104,7 @@ func Profile() http.HandlerFunc { "LikeMap": likeMap, "IsFriend": isFriend, "IsPrivate": isPrivate, - "PhotoCount": models.CountPhotos(user.ID), + "PhotoCount": models.CountPhotosICanSee(user, currentUser), } if err := tmpl.Execute(w, r, vars); err != nil { diff --git a/pkg/controller/block/block.go b/pkg/controller/block/block.go index 232c4fb..3763c41 100644 --- a/pkg/controller/block/block.go +++ b/pkg/controller/block/block.go @@ -47,6 +47,17 @@ func Blocked() http.HandlerFunc { }) } +// AddUser to manually add someone to your block list. +func AddUser() http.HandlerFunc { + tmpl := templates.Must("account/block_list_add.html") + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := tmpl.Execute(w, r, nil); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) +} + // BlockUser controller. func BlockUser() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -75,7 +86,7 @@ func BlockUser() http.HandlerFunc { user, err := models.FindUser(username) if err != nil { session.FlashError(w, r, "User Not Found") - templates.Redirect(w, "/") + templates.Redirect(w, "/users/blocklist/add") return } diff --git a/pkg/controller/photo/upload.go b/pkg/controller/photo/upload.go index e05cc43..e3871d7 100644 --- a/pkg/controller/photo/upload.go +++ b/pkg/controller/photo/upload.go @@ -167,40 +167,48 @@ func Upload() http.HandlerFunc { // Create a notification for all our Friends about a new photo. // Run in a background goroutine in case it takes a while. func notifyFriendsNewPhoto(photo *models.Photo, currentUser *models.User) { - var friendIDs []uint64 + var ( + friendIDs []uint64 + notifyUserIDs []uint64 + ) - // Who to notify? + // Get the user's literal list of friends (with explicit opt-ins). + if photo.Explicit { + friendIDs = models.FriendIDsAreExplicit(currentUser.ID) + } else { + friendIDs = models.FriendIDs(currentUser.ID) + } + + // Who to notify about this upload? if photo.Visibility == models.PhotoPrivate { // Private grantees if photo.Explicit { - friendIDs = models.PrivateGranteeAreExplicitUserIDs(currentUser.ID) - log.Info("Notify %d EXPLICIT private grantees about the new photo by %s", len(friendIDs), currentUser.Username) + notifyUserIDs = models.PrivateGranteeAreExplicitUserIDs(currentUser.ID) + log.Info("Notify %d EXPLICIT private grantees about the new photo by %s", len(notifyUserIDs), currentUser.Username) } else { - friendIDs = models.PrivateGranteeUserIDs(currentUser.ID) - log.Info("Notify %d private grantees about the new photo by %s", len(friendIDs), currentUser.Username) + notifyUserIDs = models.PrivateGranteeUserIDs(currentUser.ID) + log.Info("Notify %d private grantees about the new photo by %s", len(notifyUserIDs), currentUser.Username) } } else if photo.Visibility == models.PhotoInnerCircle { // Inner circle members. If the pic is also Explicit, further narrow to explicit friend IDs. if photo.Explicit { - friendIDs = models.FriendIDsInCircleAreExplicit(currentUser.ID) - log.Info("Notify %d EXPLICIT circle friends about the new photo by %s", len(friendIDs), currentUser.Username) + notifyUserIDs = models.FriendIDsInCircleAreExplicit(currentUser.ID) + log.Info("Notify %d EXPLICIT circle friends about the new photo by %s", len(notifyUserIDs), currentUser.Username) } else { - friendIDs = models.FriendIDsInCircle(currentUser.ID) - log.Info("Notify %d circle friends about the new photo by %s", len(friendIDs), currentUser.Username) + notifyUserIDs = models.FriendIDsInCircle(currentUser.ID) + log.Info("Notify %d circle friends about the new photo by %s", len(notifyUserIDs), currentUser.Username) } } else { - // Get all our friend IDs. If this photo is Explicit, only select - // the friends who've opted-in for Explicit photo visibility. - if photo.Explicit { - friendIDs = models.FriendIDsAreExplicit(currentUser.ID) - log.Info("Notify %d EXPLICIT friends about the new photo by %s", len(friendIDs), currentUser.Username) - } else { - friendIDs = models.FriendIDs(currentUser.ID) - log.Info("Notify %d friends about the new photo by %s", len(friendIDs), currentUser.Username) - } + // Friends only: we will notify exactly the friends we selected above. + notifyUserIDs = friendIDs } - for _, fid := range friendIDs { + // Filter down the notifyUserIDs to only include the user's friends. + // Example: someone unlocked private photos for you, but you are not their friend. + // You should not get notified about their new private photos. + notifyUserIDs = models.FilterFriendIDs(notifyUserIDs, friendIDs) + + for _, fid := range notifyUserIDs { notif := &models.Notification{ UserID: fid, AboutUser: *currentUser, diff --git a/pkg/controller/photo/user_gallery.go b/pkg/controller/photo/user_gallery.go index 97744ed..bdfc3b1 100644 --- a/pkg/controller/photo/user_gallery.go +++ b/pkg/controller/photo/user_gallery.go @@ -96,8 +96,9 @@ func UserPhotos() http.HandlerFunc { // What set of visibilities to query? visibility := []models.PhotoVisibility{models.PhotoPublic} if isOwnPhotos || isGrantee || currentUser.HasAdminScope(config.ScopePhotoModerator) { - visibility = append(visibility, models.PhotoFriends, models.PhotoPrivate) - } else if models.AreFriends(user.ID, currentUser.ID) { + visibility = append(visibility, models.PhotoPrivate) + } + if models.AreFriends(user.ID, currentUser.ID) { visibility = append(visibility, models.PhotoFriends) } @@ -146,7 +147,7 @@ func UserPhotos() http.HandlerFunc { "AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access. "User": user, "Photos": photos, - "PhotoCount": models.CountPhotos(user.ID), + "PhotoCount": models.CountPhotosICanSee(user, currentUser), "PublicPhotoCount": models.CountPublicPhotos(user.ID), "InnerCircleMinimumPublicPhotos": config.InnerCircleMinimumPublicPhotos, "Pager": pager, diff --git a/pkg/models/friend.go b/pkg/models/friend.go index 1fba397..989bf4e 100644 --- a/pkg/models/friend.go +++ b/pkg/models/friend.go @@ -105,6 +105,28 @@ func FriendIDs(userId uint64) []uint64 { 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 +} + // 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. diff --git a/pkg/models/photo.go b/pkg/models/photo.go index da21754..6c67162 100644 --- a/pkg/models/photo.go +++ b/pkg/models/photo.go @@ -146,6 +146,40 @@ func CountPhotos(userID uint64) int64 { return count } +// CountPhotosICanSee returns the number of photos on an account which can be seen by the given viewer. +func CountPhotosICanSee(user *User, viewer *User) int64 { + // Visibility filters to query by. + var visibilities = []PhotoVisibility{ + PhotoPublic, + } + + // Is the viewer in the inner circle? + if viewer.IsInnerCircle() { + visibilities = append(visibilities, PhotoInnerCircle) + } + + // Is the viewer friends with the target? + if AreFriends(user.ID, viewer.ID) { + visibilities = append(visibilities, PhotoFriends) + } + + // Is the viewer granted private access? + if IsPrivateUnlocked(user.ID, viewer.ID) { + visibilities = append(visibilities, PhotoPrivate) + } + + // Get the photo count now. + var count int64 + result := DB.Where( + "user_id = ? AND visibility IN ?", + user.ID, visibilities, + ).Model(&Photo{}).Count(&count) + if result.Error != nil { + log.Error("CountPhotosICanSee(%d, %d): %s", user.ID, viewer.ID, result.Error) + } + return count +} + // 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 { @@ -169,7 +203,7 @@ func MapPhotoCounts(users []*User) PhotoCountMap { ).Select( "user_id, count(id) AS photo_count", ).Where( - "user_id IN ?", userIDs, + "user_id IN ? AND visibility = ?", userIDs, PhotoPublic, ).Group("user_id").Scan(&groups); res.Error != nil { log.Error("CountPhotosForUsers: %s", res.Error) } @@ -182,6 +216,91 @@ func MapPhotoCounts(users []*User) PhotoCountMap { return result } +// MapPhotoCounts returns a mapping of user ID to the CountPhotosICanSee()-equivalent result for each. +// It's used on the member directory to show photo counts on each user card. +/* TODO: under construction.. +func MapPhotoCounts(users []*User, viewer *User) PhotoCountMap { + var ( + userIDs = []uint64{} + result = PhotoCountMap{} + + wheres = []string{} + placeholders = []interface{}{} + + // User ID filters for the viewer's context. + myFriendIDs = FriendIDs(viewer.ID) + myPrivateGrantedIDs = PrivateGrantedUserIDs(viewer.ID) + isInnerCircle = viewer.IsInnerCircle() + ) + + // Define "all photos visibilities" + var ( + photosPublic = []PhotoVisibility{ + PhotoPublic, + } + photosFriends = []PhotoVisibility{ + PhotoPublic, + PhotoFriends, + } + photosPrivate = []PhotoVisibility{ + PhotoPublic, + PhotoPrivate, + } + ) + if isInnerCircle { + photosPublic = append(photosPublic, PhotoInnerCircle) + photosFriends = append(photosFriends, PhotoInnerCircle) + } + + // Flatten the userIDs of all passed in users. + for _, user := range users { + userIDs = append(userIDs, user.ID) + } + + // Build the where clause. + wheres = append(wheres, "user_id IN ?") + placeholders = append(placeholders, userIDs) + + log.Error("FRIEND IDS: %+v", myFriendIDs) + + // Filter by which photos are visible to us. + wheres = append(wheres, + "((user_id IN ? AND visibility IN ?) OR "+ + "(user_id IN ? AND visibility IN ?) OR "+ + "(user_id NOT IN ? AND visibility IN ?))", + ) + placeholders = append(placeholders, + myFriendIDs, photosFriends, + myPrivateGrantedIDs, photosPrivate, + myFriendIDs, photosPublic, + ) + + type group struct { + UserID uint64 + PhotoCount int64 + } + var groups = []group{} + + if res := DB.Table( + "photos", + ).Select( + "user_id, count(id) AS photo_count", + ).Where( + strings.Join(wheres, " AND "), + placeholders..., + ).Group("user_id").Scan(&groups); res.Error != nil { + log.Error("CountPhotosForUsers: %s", res.Error) + } + + // Map the results in. + for _, row := range groups { + result[row.UserID] = row.PhotoCount + } + + return result +} +*/ + type PhotoCountMap map[uint64]int64 // Get a photo count for the given user ID from the map. diff --git a/pkg/router/router.go b/pkg/router/router.go index 6ee2ab4..4fe56f5 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -63,6 +63,7 @@ func New() http.Handler { mux.Handle("/friends/add", middleware.LoginRequired(friend.AddFriend())) mux.Handle("/users/block", middleware.LoginRequired(block.BlockUser())) mux.Handle("/users/blocked", middleware.LoginRequired(block.Blocked())) + mux.Handle("/users/blocklist/add", middleware.LoginRequired(block.AddUser())) mux.Handle("/comments", middleware.LoginRequired(comment.PostComment())) mux.Handle("/comments/subscription", middleware.LoginRequired(comment.Subscription())) mux.Handle("/admin/unimpersonate", middleware.LoginRequired(admin.Unimpersonate())) diff --git a/web/templates/account/block_list.html b/web/templates/account/block_list.html index 6b0ef32..fdb2116 100644 --- a/web/templates/account/block_list.html +++ b/web/templates/account/block_list.html @@ -17,6 +17,13 @@ (page {{.Pager.Page}} of {{.Pager.Pages}}). +
++ + Add to Block List +
+