website/pkg/models/deletion/delete_user.go
Noah Petherbridge 2f31d678d0 Usage Statistics and Website Demographics
Adds two new features to collect and show useful analytics.

Usage Statistics:
* Begin tracking daily active users who log in and interact with major features
  of the website each day, such as the chat room, forum and gallery.

Demographics page:
* For marketing, the home page now shows live statistics about the breakdown of
  content (explicit vs. non-explicit) on the site, and the /insights page gives
  a lot more data in detail.
* Show the percent split in photo gallery content and how many users opt-in or
  share explicit content on the site.
* Show high-level demographics of the members (by age range, gender, orientation)

Misc cleanup:
* Rearrange model list in data export to match the auto-create statements.
* In data exports, include the forum_memberships, push_notifications and
  usage_statistics tables.
2024-09-11 19:28:52 -07:00

420 lines
11 KiB
Go

package deletion
import (
"fmt"
"code.nonshy.com/nonshy/website/pkg/chat"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/photo"
)
// DeleteUser wipes a user and all associated data from the database.
func DeleteUser(user *models.User) error {
log.Error("BEGIN DeleteUser(%d, %s)", user.ID, user.Username)
// Clear their history on the chat room.
go func() {
i, err := chat.EraseChatHistory(user.Username)
if err != nil {
log.Error("EraseChatHistory(%s): %s", user.Username, err)
return
}
log.Error("DeleteUser(%s): Cleared chat DMs history for user (%d messages erased)", user.Username, i)
}()
// Remove all linked tables and assets.
type remover struct {
Step string
Fn func(uint64) error
}
// Blank out the user's profile photo ID to avoid conflict removing their picture.
user.RemoveProfilePhoto()
// Tables to remove. In case of any unexpected DB errors, these tables are ordered
// to remove the "safest" fields first.
var todo = []remover{
{"Admin group memberships", DeleteAdminGroupUsers},
{"Disown User Forums", DisownForums},
{"Notifications", DeleteNotifications},
{"Likes", DeleteLikes},
{"Threads", DeleteForumThreads},
{"Comments", DeleteComments},
{"Subscriptions", DeleteSubscriptions},
{"Photos", DeleteUserPhotos},
{"Private Photo Grants", DeletePrivateGrants},
{"Certification Photo", DeleteCertification},
{"Who's Nearby Locations", DeleteUserLocation},
{"Comment Photos", DeleteUserCommentPhotos},
{"Messages", DeleteUserMessages},
{"Friends", DeleteFriends},
{"Blocks", DeleteBlocks},
{"Feedbacks", DeleteFeedbacks},
{"Two Factor", DeleteTwoFactor},
{"Profile Fields", DeleteProfile},
{"User Notes", DeleteUserNotes},
{"Change Logs", DeleteChangeLogs},
{"IP Addresses", DeleteIPAddresses},
{"Push Notifications", DeletePushNotifications},
{"Forum Memberships", DeleteForumMemberships},
{"Usage Statistics", DeleteUsageStatistics},
}
for _, item := range todo {
if err := item.Fn(user.ID); err != nil {
return fmt.Errorf("%s: %s", item.Step, err)
}
}
// Remove the user itself.
return user.Delete()
}
// DeleteAdminGroupUsers scrubs data for deleting a user.
func DeleteAdminGroupUsers(userID uint64) error {
log.Error("DeleteUser: DeleteAdminGroupUsers(%d)", userID)
result := models.DB.Exec(
"DELETE FROM admin_group_users WHERE user_id = ?",
userID,
)
return result.Error
}
// DeleteUserPhotos scrubs data for deleting a user.
func DeleteUserPhotos(userID uint64) error {
log.Error("DeleteUser: BEGIN DeleteUserPhotos(%d)", userID)
// Deeply scrub all user photos.
pager := &models.Pagination{
Page: 1,
PerPage: 20,
Sort: "photos.id",
}
for {
photos, err := models.PaginateUserPhotos(
userID,
models.UserGallery{
Visibility: models.PhotoVisibilityAll,
},
pager,
)
if err != nil {
return err
}
if len(photos) == 0 {
break
}
for _, item := range photos {
log.Warn("DeleteUserPhotos(%d): remove file %s", userID, item.Filename)
photo.Delete(item.Filename)
if item.CroppedFilename != "" {
log.Warn("DeleteUserPhotos(%d): remove file %s", userID, item.CroppedFilename)
photo.Delete(item.CroppedFilename)
}
if err := item.Delete(); err != nil {
return err
}
}
}
log.Error("DeleteUser: END DeleteUserPhotos(%d)", userID)
return nil
}
// DeleteUserCommentPhotos scrubs data for deleting a user.
func DeleteUserCommentPhotos(userID uint64) error {
log.Error("DeleteUser: BEGIN DeleteUserCommentPhotos(%d)", userID)
// Deeply scrub all user photos.
pager := &models.Pagination{
Page: 1,
PerPage: 20,
Sort: "comment_photos.id",
}
for {
photos, err := models.PaginateUserCommentPhotos(
userID,
pager,
)
if err != nil {
return err
}
if len(photos) == 0 {
break
}
for _, item := range photos {
log.Warn("DeleteUserCommentPhotos(%d): remove file %s", userID, item.Filename)
photo.Delete(item.Filename)
if err := item.Delete(); err != nil {
return err
}
}
}
log.Error("DeleteUser: END DeleteUserPhotos(%d)", userID)
return nil
}
// DeleteCertification scrubs data for deleting a user.
func DeleteCertification(userID uint64) error {
log.Error("DeleteUser: DeleteCertification(%d)", userID)
if cert, err := models.GetCertificationPhoto(userID); err == nil {
if cert.Filename != "" {
log.Warn("DeleteCertification(%d): remove file %s", userID, cert.Filename)
photo.Delete(cert.Filename)
}
return cert.Delete()
}
return nil
}
// DeleteTwoFactor scrubs data for deleting a user.
func DeleteTwoFactor(userID uint64) error {
log.Error("DeleteUser: DeleteTwoFactor(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.TwoFactor{})
return result.Error
}
// DeleteUserLocation scrubs data for deleting a user.
func DeleteUserLocation(userID uint64) error {
log.Error("DeleteUser: DeleteUserLocation(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.UserLocation{})
return result.Error
}
// DeleteUserMessages scrubs data for deleting a user.
func DeleteUserMessages(userID uint64) error {
log.Error("DeleteUser: DeleteUserMessages(%d)", userID)
result := models.DB.Where(
"source_user_id = ? OR target_user_id = ?",
userID, userID,
).Delete(&models.Message{})
return result.Error
}
// DeleteFriends scrubs data for deleting a user.
func DeleteFriends(userID uint64) error {
log.Error("DeleteUser: DeleteUserFriends(%d)", userID)
result := models.DB.Where(
"source_user_id = ? OR target_user_id = ?",
userID, userID,
).Delete(&models.Friend{})
return result.Error
}
// DeleteBlocks scrubs data for deleting a user.
func DeleteBlocks(userID uint64) error {
log.Error("DeleteUser: DeleteBlocks(%d)", userID)
result := models.DB.Where(
"source_user_id = ? OR target_user_id = ?",
userID, userID,
).Delete(&models.Block{})
return result.Error
}
// DeleteFeedbacks scrubs data for deleting a user.
func DeleteFeedbacks(userID uint64) error {
log.Error("DeleteUser: DeleteFeedbacks(%d)", userID)
result := models.DB.Where(
"user_id = ? OR (table_name='users' AND table_id=?)",
userID, userID,
).Delete(&models.Feedback{})
return result.Error
}
// DeletePrivateGrants scrubs data for deleting a user.
func DeletePrivateGrants(userID uint64) error {
log.Error("DeleteUser: DeletePrivateGrants(%d)", userID)
result := models.DB.Where(
"source_user_id = ? OR target_user_id = ?",
userID, userID,
).Delete(&models.PrivatePhoto{})
return result.Error
}
// DeleteNotifications scrubs all notifications about a user.
func DeleteNotifications(userID uint64) error {
log.Error("DeleteUser: DeleteNotifications(%d)", userID)
result := models.DB.Where(
"user_id = ? OR about_user_id = ?",
userID, userID,
).Delete(&models.Notification{})
return result.Error
}
// DeleteSubscriptions scrubs all notification subscriptions about a user.
func DeleteSubscriptions(userID uint64) error {
log.Error("DeleteUser: DeleteSubscriptions(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.Subscription{})
return result.Error
}
// DeleteLikes scrubs all Likes about a user.
func DeleteLikes(userID uint64) error {
log.Error("DeleteUser: DeleteLikes(%d)", userID)
result := models.DB.Where(
"user_id = ? OR (table_name='users' AND table_id=?)",
userID, userID,
).Delete(&models.Like{})
return result.Error
}
// DeleteProfile scrubs data for deleting a user.
func DeleteProfile(userID uint64) error {
log.Error("DeleteUser: DeleteProfile(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.ProfileField{})
return result.Error
}
// DeleteForumThreads scrubs all forum threads started by the user.
func DeleteForumThreads(userID uint64) error {
log.Error("DeleteUser: DeleteForumThreads(%d)", userID)
var threadIDs = []uint64{}
result := models.DB.Table(
"threads",
).Joins(
"JOIN comments ON (threads.comment_id = comments.id)",
).Select(
"distinct(threads.id) as id",
).Where(
"comments.user_id = ?",
userID,
).Scan(&threadIDs)
if result.Error != nil {
return fmt.Errorf("Couldn't list thread IDs created by user: %s", result.Error)
}
log.Warn("thread IDs to wipe: %+v", threadIDs)
// Wipe all these threads and their comments.
if len(threadIDs) > 0 {
// First, delete the threads - so they won't be referring to comment_ids that we
// delete next and causing errors to arise.
result = models.DB.Where(
"id IN ?",
threadIDs,
).Delete(&models.Thread{})
if result.Error != nil {
return fmt.Errorf("Couldn't delete your forum threads: %s", result.Error)
}
// Remove the comments.
result = models.DB.Where(
"table_name = ? AND table_id IN ?",
"threads", threadIDs,
).Delete(&models.Comment{})
if result.Error != nil {
return fmt.Errorf("Couldn't wipe threads of comments: %s", result.Error)
}
return result.Error
}
return nil
}
// DeleteComments deletes all comments by the user.
func DeleteComments(userID uint64) error {
log.Error("DeleteUser: DeleteComments(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.Comment{})
return result.Error
}
// DeleteUserNotes scrubs data for deleting a user.
func DeleteUserNotes(userID uint64) error {
log.Error("DeleteUser: DeleteUserNotes(%d)", userID)
result := models.DB.Where(
"user_id = ? OR about_user_id = ?",
userID, userID,
).Delete(&models.UserNote{})
return result.Error
}
// DeleteChangeLogs scrubs data for deleting a user.
func DeleteChangeLogs(userID uint64) error {
log.Error("DeleteUser: DeleteChangeLogs(%d)", userID)
result := models.DB.Where(
"about_user_id = ?",
userID,
).Delete(&models.ChangeLog{})
return result.Error
}
// DeleteIPAddresses scrubs data for deleting a user.
func DeleteIPAddresses(userID uint64) error {
log.Error("DeleteUser: DeleteIPAddresses(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.IPAddress{})
return result.Error
}
// DeletePushNotifications scrubs data for deleting a user.
func DeletePushNotifications(userID uint64) error {
log.Error("DeleteUser: DeletePushNotifications(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.PushNotification{})
return result.Error
}
// DisownForums unlinks the user from their owned forums.
func DisownForums(userID uint64) error {
log.Error("DeleteUser: DisownForums(%d)", userID)
result := models.DB.Exec(`
UPDATE forums
SET owner_id = NULL
WHERE owner_id = ?
`, userID)
return result.Error
}
// DeleteForumMemberships scrubs data for deleting a user.
func DeleteForumMemberships(userID uint64) error {
log.Error("DeleteUser: DeleteForumMemberships(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.ForumMembership{})
return result.Error
}
// DeleteUsageStatistics scrubs data for deleting a user.
func DeleteUsageStatistics(userID uint64) error {
log.Error("DeleteUser: DeleteUsageStatistics(%d)", userID)
result := models.DB.Where(
"user_id = ?",
userID,
).Delete(&models.UsageStatistic{})
return result.Error
}