From 542d0bb3003dd28c4aedcd408a4c55d79c540f23 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Tue, 1 Oct 2024 20:44:11 -0700 Subject: [PATCH] Improvements on community flagged explicit photos When a user marks that another photo should have been marked as explicit: * The owner of that photo gets a notification about it, which reminds them of the explicit photo policy. * The photo's "Flagged" boolean is set (along with the Explicit boolean) * The 'Edit' page on a Flagged photo shows a red banner above the Explicit option, explaining that it was flagged. The checkbox text is crossed-out, with a "no" cursor and title text over - but can still be unchecked. If the user removes the Explicit flag on a flagged photo and saves it: * An admin report is generated to notify to take a look too. * The Explicit flag is cleared as normal * The Flagged boolean is also cleared on this photo: if they set it back to Explicit again themselves, the red banner won't appear and it won't notify again - unless a community member flagged it again! Also makes some improvements to the admin page: * On photo reports: show a blurred-out (clickable to reveal) photo on feedback items about photos. --- pkg/controller/admin/feedback.go | 16 +++++++- pkg/controller/admin/user_actions.go | 1 + pkg/controller/api/mark_explicit.go | 18 +++++++++ pkg/controller/photo/edit_delete.go | 28 +++++++++++++ pkg/models/notification.go | 1 + pkg/models/notification_filters.go | 8 +++- pkg/models/photo.go | 48 ++++++++++++++++++++++ web/static/css/theme.css | 4 ++ web/templates/account/dashboard.html | 17 ++++++++ web/templates/admin/feedback.html | 23 ++++++++--- web/templates/faq.html | 6 +++ web/templates/photo/upload.html | 59 +++++++++++++++++++++------- web/templates/tos.html | 19 +++++++-- 13 files changed, 224 insertions(+), 24 deletions(-) diff --git a/pkg/controller/admin/feedback.go b/pkg/controller/admin/feedback.go index 2767c0a..de96f21 100644 --- a/pkg/controller/admin/feedback.go +++ b/pkg/controller/admin/feedback.go @@ -183,17 +183,30 @@ func Feedback() http.HandlerFunc { } // Map user IDs. - var userIDs = []uint64{} + var ( + userIDs = []uint64{} + photoIDs = []uint64{} + ) for _, p := range page { if p.UserID > 0 { userIDs = append(userIDs, p.UserID) } + + if p.TableName == "photos" && p.TableID > 0 { + photoIDs = append(photoIDs, p.TableID) + } } userMap, err := models.MapUsers(currentUser, userIDs) if err != nil { session.FlashError(w, r, "Couldn't map user IDs: %s", err) } + // Map photo IDs. + photoMap, err := models.MapPhotos(photoIDs) + if err != nil { + session.FlashError(w, r, "Couldn't map photo IDs: %s", err) + } + var vars = map[string]interface{}{ // Filter settings. "DistinctSubjects": models.DistinctFeedbackSubjects(), @@ -205,6 +218,7 @@ func Feedback() http.HandlerFunc { "Acknowledged": acknowledged, "Feedback": page, "UserMap": userMap, + "PhotoMap": photoMap, "Pager": pager, } if err := tmpl.Execute(w, r, vars); err != nil { diff --git a/pkg/controller/admin/user_actions.go b/pkg/controller/admin/user_actions.go index de32497..2200161 100644 --- a/pkg/controller/admin/user_actions.go +++ b/pkg/controller/admin/user_actions.go @@ -52,6 +52,7 @@ func MarkPhotoExplicit() http.HandlerFunc { } photo.Explicit = true + photo.Flagged = true if err := photo.Save(); err != nil { session.FlashError(w, r, "Couldn't save photo: %s", err) } else { diff --git a/pkg/controller/api/mark_explicit.go b/pkg/controller/api/mark_explicit.go index ca9f819..b9f0290 100644 --- a/pkg/controller/api/mark_explicit.go +++ b/pkg/controller/api/mark_explicit.go @@ -83,6 +83,7 @@ func MarkPhotoExplicit() http.HandlerFunc { } photo.Explicit = true + photo.Flagged = true if err := photo.Save(); err != nil { SendJSON(w, http.StatusBadRequest, Response{ Error: fmt.Sprintf("Couldn't save the photo: %s", err), @@ -90,6 +91,23 @@ func MarkPhotoExplicit() http.HandlerFunc { return } + // Send the photo's owner a notification so they are aware. + if owner, err := models.GetUser(photo.UserID); err == nil { + notif := &models.Notification{ + UserID: owner.ID, + AboutUser: *owner, + Type: models.NotificationExplicitPhoto, + TableName: "photos", + TableID: photo.ID, + Link: fmt.Sprintf("/photo/view?id=%d", photo.ID), + } + if err := models.CreateNotification(notif); err != nil { + log.Error("Couldn't create Likes notification: %s", err) + } + } else { + log.Error("MarkExplicit: getting photo owner (%d): %s", photo.UserID, err) + } + // If a non-admin user has hit this API, log an admin report for visibility and // to keep a pulse on things (e.g. in case of abuse). if !currentUser.IsAdmin { diff --git a/pkg/controller/photo/edit_delete.go b/pkg/controller/photo/edit_delete.go index 9979fcf..a88bd42 100644 --- a/pkg/controller/photo/edit_delete.go +++ b/pkg/controller/photo/edit_delete.go @@ -88,6 +88,9 @@ func Edit() http.HandlerFunc { // Are we GOING private? goingPrivate = visibility == models.PhotoPrivate && visibility != photo.Visibility + + // Is the user fighting an 'Explicit' tag added by the community? + isFightingExplicitFlag = photo.Flagged && photo.Explicit && !isExplicit ) if len(altText) > config.AltTextMaxLength { @@ -141,6 +144,31 @@ func Edit() http.HandlerFunc { setProfilePic = false } + // If the user is fighting a recent Explicit flag from the community. + if isFightingExplicitFlag { + + // Notify the admin (unless we are an admin). + if !requestUser.IsAdmin { + fb := &models.Feedback{ + Intent: "report", + Subject: "Explicit photo flag dispute", + UserID: currentUser.ID, + TableName: "photos", + TableID: photo.ID, + Message: "A user's photo was recently **flagged by the community** as Explicit, and its owner " + + "has **removed** the Explicit setting.\n\n" + + "Please check out the photo below and verify what its Explicit setting should be:", + } + + if err := models.CreateFeedback(fb); err != nil { + log.Error("Couldn't save feedback from user updating their DOB: %s", err) + } + } + + // Allow this change but clear the Flagged status. + photo.Flagged = false + } + if err := photo.Save(); err != nil { session.FlashError(w, r, "Couldn't save photo: %s", err) } diff --git a/pkg/models/notification.go b/pkg/models/notification.go index 32b73a1..bf5c708 100644 --- a/pkg/models/notification.go +++ b/pkg/models/notification.go @@ -46,6 +46,7 @@ const ( NotificationPrivatePhoto NotificationType = "private_photo" // private photo grants NotificationNewPhoto NotificationType = "new_photo" NotificationForumModerator NotificationType = "forum_moderator" // appointed as a forum moderator + NotificationExplicitPhoto NotificationType = "explicit_photo" // user photo was flagged explicit NotificationCustom NotificationType = "custom" // custom message pushed ) diff --git a/pkg/models/notification_filters.go b/pkg/models/notification_filters.go index a107b8a..9368ab3 100644 --- a/pkg/models/notification_filters.go +++ b/pkg/models/notification_filters.go @@ -85,7 +85,13 @@ func (nf NotificationFilter) Query() (where string, placeholders []interface{}, types = append(types, NotificationPrivatePhoto) } if nf.Misc { - types = append(types, NotificationFriendApproved, NotificationCertApproved, NotificationCertRejected, NotificationCustom) + types = append(types, + NotificationFriendApproved, + NotificationCertApproved, + NotificationCertRejected, + NotificationExplicitPhoto, + NotificationCustom, + ) } return "type IN ?", types, true diff --git a/pkg/models/photo.go b/pkg/models/photo.go index ddbc818..8e8e64e 100644 --- a/pkg/models/photo.go +++ b/pkg/models/photo.go @@ -274,6 +274,54 @@ func GetOrphanedPhotos() ([]*Photo, int64, error) { return ps, count, res.Error } +// PhotoMap helps map a set of users to look up by ID. +type PhotoMap map[uint64]*Photo + +// MapPhotos looks up a set of photos IDs in bulk and returns a PhotoMap suitable for templates. +func MapPhotos(photoIDs []uint64) (PhotoMap, error) { + var ( + photoMap = PhotoMap{} + set = map[uint64]interface{}{} + distinct = []uint64{} + ) + + // Uniqueify the IDs. + for _, uid := range photoIDs { + if _, ok := set[uid]; ok { + continue + } + set[uid] = nil + distinct = append(distinct, uid) + } + + var ( + photos = []*Photo{} + result = DB.Model(&Photo{}).Where("id IN ?", distinct).Find(&photos) + ) + + if result.Error == nil { + for _, row := range photos { + photoMap[row.ID] = row + } + } + + return photoMap, result.Error +} + +// Has a photo ID in the map? +func (pm PhotoMap) Has(id uint64) bool { + _, ok := pm[id] + return ok +} + +// Get a photo from the PhotoMap. +func (pm PhotoMap) Get(id uint64) *Photo { + if photo, ok := pm[id]; ok { + return photo + } + return nil +} + /* IsSiteGalleryThrottled returns whether the user is throttled from marking additional pictures for the Site Gallery. diff --git a/web/static/css/theme.css b/web/static/css/theme.css index 4d83e54..2c263d9 100644 --- a/web/static/css/theme.css +++ b/web/static/css/theme.css @@ -16,6 +16,10 @@ abbr { cursor: default; } +.has-text-smaller { + font-size: smaller; +} + img { /* https://stackoverflow.com/questions/12906789/preventing-an-image-from-being-draggable-or-selectable-without-using-js */ user-drag: none; diff --git a/web/templates/account/dashboard.html b/web/templates/account/dashboard.html index 4e0fbc9..fb5a78b 100644 --- a/web/templates/account/dashboard.html +++ b/web/templates/account/dashboard.html @@ -600,6 +600,11 @@ You have been appointed as a moderator for the forum {{$Body.Forum.Title}}! + {{else if eq .Type "explicit_photo"}} + + + Your photo was marked as Explicit! + {{else}} {{.AboutUser.Username}} {{.Type}} {{.TableName}} {{.TableID}} {{end}} @@ -628,6 +633,18 @@ See all comments + {{else if eq .Type "explicit_photo"}} +
+

+ A community member thinks that this photo should have been marked as 'Explicit' when + it was uploaded. +

+

+ Please review our Explicit Photos policy + and remember to correctly mark your new uploads as 'explicit' when they contain sexually + suggestive content. +

+
{{else}} {{or $Body.Photo.Caption "No caption."}} {{end}} diff --git a/web/templates/admin/feedback.html b/web/templates/admin/feedback.html index ce2ae4e..465b4f8 100644 --- a/web/templates/admin/feedback.html +++ b/web/templates/admin/feedback.html @@ -223,11 +223,24 @@
- {{if eq .Message ""}} -

No message attached.

- {{else}} - {{ToMarkdown .Message}} - {{end}} + {{if eq .Message ""}} +

No message attached.

+ {{else}} + {{ToMarkdown .Message}} + {{end}} + + + {{if eq .TableName "photos"}} + {{$Photo := $Root.PhotoMap.Get .TableID}} + {{if $Photo}} +
+ + + +
+ {{end}} + {{end}}
diff --git a/web/templates/faq.html b/web/templates/faq.html index 52adb6d..8595933 100644 --- a/web/templates/faq.html +++ b/web/templates/faq.html @@ -701,6 +701,12 @@ content from other users -- by default this site is "normal nudes" friendly!

+

+ Please see the Explicit Photos & Sexual Content + policy on our Terms of Service for some examples when a photo should + be marked as 'explicit.' +

+

Are digitally altered or 'photoshopped' pictures okay?

diff --git a/web/templates/photo/upload.html b/web/templates/photo/upload.html index a6c207c..a96f6c1 100644 --- a/web/templates/photo/upload.html +++ b/web/templates/photo/upload.html @@ -264,7 +264,6 @@ + + + {{$IsFlagged := and .EditPhoto .EditPhoto.Flagged .EditPhoto.Explicit}} + {{if $IsFlagged}} +

+

+ + + Notice: + + This photo was classified by the community as containing 'Explicit' content. +

+ +

+ Please review what {{.Title}} considers an 'Explicit' photo + and if this photo fits the description, please leave this photo with the + 'Explicit' box checked, below. +

+ +

+ If you disagree that this photo should have been marked as 'Explicit,' you MAY + uncheck the box below and remove the Explicit status. Note: the website admin will + be notified to take a look as well if you do this, to verify that your photo has the correct 'Explicit' + setting. +

+
+ {{end}} + {{if eq .Intent "profile_pic"}} Your default profile picture should @@ -405,19 +432,23 @@ that to your page, just not as your default profile picture!

{{else}} - -

- Mark this box if this photo contains any explicit content, including an - erect penis, close-up of genitalia, or any depiction of sexual activity. - Use your best judgment. "Normal nudes" such as full body nudes in a - non-sexual context do not need to check this box. -

+ +

+ Mark this box if this photo contains any explicit content, including an + erect penis, close-up of genitalia, or any depiction of sexual activity. + Use your best judgment. "Normal nudes" such as full body nudes in a + non-sexual context do not need to check this box. +

{{end}} diff --git a/web/templates/tos.html b/web/templates/tos.html index 2c0ba60..a15b7dd 100644 --- a/web/templates/tos.html +++ b/web/templates/tos.html @@ -380,20 +380,20 @@

- A photo is considered "explicit" if it depicts any of the following features: + A photo is considered "explicit" if it depicts any of the following features:

+

+ As a general rule of thumb: if a picture could be reasonably considered to be "porn" then you should + mark it as Explicit when uploading it to your gallery. +

+ +

+ Important: extreme and commonly offensive content (such as fisting, gaping or prolapsed + ass holes, etc.) are NOT permitted on {{PrettyTitle}}. Please review the following + section for a list of Prohibited Content. +

+

Prohibited Content

@@ -414,6 +425,7 @@