website/pkg/models/exporting/models.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

495 lines
12 KiB
Go

package exporting
import (
"archive/zip"
"fmt"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/models"
"gorm.io/gorm"
)
// ExportModels is the entry point function to export all data tables about a user.
func ExportModels(zw *zip.Writer, user *models.User) error {
type task struct {
Step string
Fn func(*zip.Writer, *models.User) error
}
// List of tables to export. Keep the ordering in sync with
// the AutoMigrate() calls in ../models.go
var todo = []task{
// Note: AdminGroup info is eager-loaded in User export
{"Block", ExportBlockTable},
{"CertificationPhoto", ExportCertificationPhotoTable},
{"ChangeLog", ExportChangeLogTable},
{"Comment", ExportCommentTable},
{"CommentPhoto", ExportCommentPhotoTable},
{"Feedback", ExportFeedbackTable},
{"ForumMembership", ExportForumMembershipTable},
{"Friend", ExportFriendTable},
{"Forum", ExportForumTable},
{"IPAddress", ExportIPAddressTable},
{"Like", ExportLikeTable},
{"Message", ExportMessageTable},
{"Notification", ExportNotificationTable},
{"ProfileField", ExportProfileFieldTable},
{"Photo", ExportPhotoTable},
// Note: Poll table is eager-loaded in Thread export
{"PollVote", ExportPollVoteTable},
{"PrivatePhoto", ExportPrivatePhotoTable},
{"PushNotification", ExportPushNotificationTable},
{"Subscription", ExportSubscriptionTable},
{"Thread", ExportThreadTable},
{"TwoFactor", ExportTwoFactorTable},
{"UsageStatistic", ExportUsageStatisticTable},
{"User", ExportUserTable},
{"UserLocation", ExportUserLocationTable},
{"UserNote", ExportUserNoteTable},
}
for _, item := range todo {
log.Info("Exporting data model: %s", item.Step)
if err := item.Fn(zw, user); err != nil {
return fmt.Errorf("%s: %s", item.Step, err)
}
}
return nil
}
func ExportUserTable(zw *zip.Writer, user *models.User) error {
return ZipJson(zw, "user.json", user)
}
func ExportProfileFieldTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.ProfileField{}
query = models.DB.Model(&models.ProfileField{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "profile_fields.json", items)
}
func ExportPhotoTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Photo{}
query = models.DB.Model(&models.Photo{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
// Copy all the images into the ZIP.
for _, row := range items {
if row.Filename != "" {
if err := ZipPhoto(zw, "photos", row.Filename); err != nil {
return err
}
}
if row.CroppedFilename != "" {
if err := ZipPhoto(zw, "profile_photos", row.CroppedFilename); err != nil {
return err
}
}
}
return ZipJson(zw, "photos.json", items)
}
func ExportPrivatePhotoTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.PrivatePhoto{}
query = models.DB.Model(&models.PrivatePhoto{}).Where(
"source_user_id = ? OR target_user_id = ?",
user.ID, user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "private_photos.json", items)
}
func ExportCertificationPhotoTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.CertificationPhoto{}
query = models.DB.Model(&models.CertificationPhoto{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
// Copy all the images into the ZIP.
for _, row := range items {
if row.Filename != "" {
if err := ZipPhoto(zw, "certification_photo", row.Filename); err != nil {
return err
}
}
}
return ZipJson(zw, "certification_photo.json", items)
}
func ExportMessageTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Message{}
query = models.DB.Model(&models.Message{}).Where(
"source_user_id = ? OR target_user_id = ?",
user.ID, user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "messages.json", items)
}
func ExportFriendTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Friend{}
query = models.DB.Model(&models.Friend{}).Where(
"source_user_id = ? OR target_user_id = ?",
user.ID, user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "friends.json", items)
}
func ExportBlockTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Block{}
query = models.DB.Model(&models.Block{}).Where(
"source_user_id = ? OR target_user_id = ?",
user.ID, user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "blocks.json", items)
}
func ExportFeedbackTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Feedback{}
photoIDs, _ = user.AllPhotoIDs()
query *gorm.DB
)
// If they have photos, query on those.
if len(photoIDs) > 0 {
query = models.DB.Model(&models.Feedback{}).Where(
"user_id = ? OR (table_name = 'users' AND table_id = ?) OR (table_name = 'photos' AND table_id IN ?)",
user.ID, user.ID, photoIDs,
).Find(&items)
} else {
// Only reports about their user.
query = models.DB.Model(&models.Feedback{}).Where(
"user_id = ? OR (table_name = 'users' AND table_id = ?)",
user.ID, user.ID,
).Find(&items)
}
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "feedback.json", items)
}
func ExportForumTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Forum{}
query = models.DB.Model(&models.Forum{}).Where(
"owner_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "forums.json", items)
}
func ExportThreadTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Thread{}
query = (&models.Thread{}).Preload().Joins(
"JOIN comments ON (comments.id = threads.comment_id)",
).Where(
"comments.user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "threads.json", items)
}
func ExportCommentTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Comment{}
photoIDs, _ = user.AllPhotoIDs()
query *gorm.DB
)
// If they have photos, query on those.
if len(photoIDs) > 0 {
query = models.DB.Model(&models.Comment{}).Where(
"user_id = ? OR (table_name = 'users' AND table_id = ?) OR (table_name = 'photos' AND table_id IN ?)",
user.ID, user.ID, photoIDs,
).Find(&items)
} else {
query = models.DB.Model(&models.Comment{}).Where(
"user_id = ? OR (table_name = 'users' AND table_id = ?)",
user.ID, user.ID,
).Find(&items)
}
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "comments.json", items)
}
func ExportLikeTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Like{}
photoIDs, _ = user.AllPhotoIDs()
query *gorm.DB
)
// If they have photos, query on those.
if len(photoIDs) > 0 {
query = models.DB.Model(&models.Like{}).Where(
"user_id = ? OR (table_name = 'users' AND table_id = ?) OR (table_name = 'photos' AND table_id IN ?)",
user.ID, user.ID, photoIDs,
).Find(&items)
} else {
// Only reports about their user.
query = models.DB.Model(&models.Like{}).Where(
"user_id = ? OR (table_name = 'users' AND table_id = ?)",
user.ID, user.ID,
).Find(&items)
}
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "likes.json", items)
}
func ExportNotificationTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Notification{}
query = models.DB.Model(&models.Notification{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "notifications.json", items)
}
func ExportSubscriptionTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.Subscription{}
query = models.DB.Model(&models.Subscription{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "subscriptions.json", items)
}
func ExportCommentPhotoTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.CommentPhoto{}
query = models.DB.Model(&models.CommentPhoto{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
// Copy all the images into the ZIP.
for _, row := range items {
if row.Filename != "" {
if err := ZipPhoto(zw, "comment_photos", row.Filename); err != nil {
return err
}
}
}
return ZipJson(zw, "comment_photos.json", items)
}
func ExportPollVoteTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.PollVote{}
query = (&models.PollVote{}).Preload().Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "poll_votes.json", items)
}
func ExportUserNoteTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.UserNote{}
query = models.DB.Model(&models.UserNote{}).Where(
"user_id = ? OR about_user_id = ?",
user.ID, user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "user_notes.json", items)
}
func ExportChangeLogTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.ChangeLog{}
query = models.DB.Model(&models.ChangeLog{}).Where(
"about_user_id = ? OR admin_user_id = ?",
user.ID, user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "change_logs.json", items)
}
func ExportUserLocationTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.UserLocation{}
query = models.DB.Model(&models.UserLocation{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "user_location.json", items)
}
func ExportTwoFactorTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.TwoFactor{}
query = models.DB.Model(&models.TwoFactor{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "two_factor.json", items)
}
func ExportIPAddressTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.IPAddress{}
query = models.DB.Model(&models.IPAddress{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "ip_addresses.json", items)
}
func ExportForumMembershipTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.ForumMembership{}
query = models.DB.Model(&models.ForumMembership{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "forum_memberships.json", items)
}
func ExportPushNotificationTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.PushNotification{}
query = models.DB.Model(&models.PushNotification{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "push_notifications.json", items)
}
func ExportUsageStatisticTable(zw *zip.Writer, user *models.User) error {
var (
items = []*models.UsageStatistic{}
query = models.DB.Model(&models.UsageStatistic{}).Where(
"user_id = ?",
user.ID,
).Find(&items)
)
if query.Error != nil {
return query.Error
}
return ZipJson(zw, "usage_statistics.json", items)
}