website/pkg/models/blocklist.go

343 lines
8.1 KiB
Go

package models
import (
"fmt"
"time"
"code.nonshy.com/nonshy/website/pkg/log"
"gorm.io/gorm"
)
// Block table.
type Block struct {
ID uint64 `gorm:"primaryKey"`
SourceUserID uint64 `gorm:"index"`
TargetUserID uint64 `gorm:"index"`
CreatedAt time.Time
UpdatedAt time.Time
}
// AddBlock is sourceUserId adding targetUserId to their block list.
func AddBlock(sourceUserID, targetUserID uint64) error {
// Unfriend in the process.
RemoveFriend(sourceUserID, targetUserID)
// Did we already block this user?
var b *Block
forward := DB.Where(
"source_user_id = ? AND target_user_id = ?",
sourceUserID, targetUserID,
).First(&b).Error
// Update existing.
if forward == nil {
return nil
}
// Create the block.
b = &Block{
SourceUserID: sourceUserID,
TargetUserID: targetUserID,
}
return DB.Create(b).Error
}
// IsBlocking quickly sees if either user blocks the other.
func IsBlocking(sourceUserID, targetUserID uint64) bool {
b := &Block{}
result := DB.Where(
"(source_user_id = ? AND target_user_id = ?) OR "+
"(target_user_id = ? AND source_user_id = ?)",
sourceUserID, targetUserID,
sourceUserID, targetUserID,
).First(&b)
return result.Error == nil
}
// IsBlocked quickly checks if sourceUserID currently blocks targetUserID.
func IsBlocked(sourceUserID, targetUserID uint64) bool {
b := &Block{}
result := DB.Where(
"source_user_id = ? AND target_user_id = ?",
sourceUserID, targetUserID,
).First(&b)
return result.Error == nil
}
// PaginateBlockList views a user's blocklist.
func PaginateBlockList(user *User, pager *Pagination) ([]*User, error) {
// We paginate over the Block table.
var (
bs = []*Block{}
userIDs = []uint64{}
query *gorm.DB
)
query = DB.Where(
"source_user_id = ?",
user.ID,
)
query = query.Order(pager.Sort)
query.Model(&Block{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&bs)
if result.Error != nil {
return nil, result.Error
}
// Now of these friends get their User objects.
for _, b := range bs {
userIDs = append(userIDs, b.TargetUserID)
}
return GetUsers(user, userIDs)
}
// BlockedUserIDs returns all user IDs blocked by the user (bidirectional, source or target user).
func BlockedUserIDs(user *User) []uint64 {
// Have we looked this up already on this request?
if user.cacheBlockedUserIDs != nil {
return user.cacheBlockedUserIDs
}
var (
bs = []*Block{}
userIDs = []uint64{}
)
DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Find(&bs)
for _, row := range bs {
for _, uid := range []uint64{row.TargetUserID, row.SourceUserID} {
if uid != user.ID {
userIDs = append(userIDs, uid)
}
}
}
// Cache the result in the User so we don't query it again.
user.cacheBlockedUserIDs = userIDs
return userIDs
}
// MapBlockedUserIDs returns BlockedUserIDs as a lookup hashmap (not for front-end templates currently).
func MapBlockedUserIDs(user *User) map[uint64]interface{} {
var (
result = map[uint64]interface{}{}
blockedIDs = BlockedUserIDs(user)
)
for _, uid := range blockedIDs {
result[uid] = nil
}
return result
}
// FilterBlockingUserIDs narrows down a set of User IDs to remove ones that block (or are blocked by) the current user.
func FilterBlockingUserIDs(currentUser *User, userIDs []uint64) []uint64 {
var (
// Get the IDs to exclude.
blockedIDs = MapBlockedUserIDs(currentUser)
// Filter the result.
result = []uint64{}
)
for _, uid := range userIDs {
if _, ok := blockedIDs[uid]; ok {
continue
}
result = append(result, uid)
}
return result
}
// BlockedUserIDsByUser returns all user IDs blocked by the user (one directional only)
func BlockedUserIDsByUser(userId uint64) []uint64 {
var (
bs = []*Block{}
userIDs = []uint64{}
)
DB.Where("source_user_id = ?", userId).Find(&bs)
for _, row := range bs {
for _, uid := range []uint64{row.TargetUserID, row.SourceUserID} {
if uid != userId {
userIDs = append(userIDs, uid)
}
}
}
return userIDs
}
// GetAllBlockedUserIDs returns the forward and reverse lists of blocked user IDs for the user.
func GetAllBlockedUserIDs(user *User) (forward, reverse []uint64) {
var (
bs = []*Block{}
)
DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Find(&bs)
for _, row := range bs {
if row.SourceUserID == user.ID {
forward = append(forward, row.TargetUserID)
} else if row.TargetUserID == user.ID {
reverse = append(reverse, row.SourceUserID)
}
}
return forward, reverse
}
// BulkRestoreBlockedUserIDs inserts many blocked user IDs in one query.
//
// Returns the count of blocks added.
func BulkRestoreBlockedUserIDs(user *User, forward, reverse []uint64) (int, error) {
var bs = []*Block{}
// Forward list.
for _, uid := range forward {
bs = append(bs, &Block{
SourceUserID: user.ID,
TargetUserID: uid,
})
}
// Reverse list.
for _, uid := range reverse {
bs = append(bs, &Block{
SourceUserID: uid,
TargetUserID: user.ID,
})
}
// Anything to do?
if len(bs) == 0 {
return 0, nil
}
// Batch create.
res := DB.Create(bs)
return len(bs), res.Error
}
// BlockedUsernames returns all usernames blocked by (or blocking) the user.
func BlockedUsernames(user *User) []string {
var (
userIDs = BlockedUserIDs(user)
usernames = []string{}
)
if len(userIDs) == 0 {
return usernames
}
if res := DB.Table(
"users",
).Select(
"username",
).Where(
"id IN ?", userIDs,
).Scan(&usernames); res.Error != nil {
log.Error("BlockedUsernames(%s): %s", user.Username, res.Error)
}
return usernames
}
// GetBlocklistInsights returns detailed block lists (both directions) about a user, for admin insight.
func GetBlocklistInsights(user *User) (*BlocklistInsight, error) {
// Collect ALL user IDs (both directions) of this user's blocklist.
var (
bs = []*Block{}
forward = []*Block{} // Users they block
reverse = []*Block{} // Users who block the target
userIDs = []uint64{user.ID}
usernames = map[uint64]string{}
admins = map[uint64]bool{}
)
// Get the complete blocklist and bucket them into forward and reverse.
DB.Where("source_user_id = ? OR target_user_id = ?", user.ID, user.ID).Order("created_at desc").Find(&bs)
for _, row := range bs {
if row.SourceUserID == user.ID {
forward = append(forward, row)
userIDs = append(userIDs, row.TargetUserID)
} else {
reverse = append(reverse, row)
userIDs = append(userIDs, row.SourceUserID)
}
}
// Map all the user IDs to user names.
if len(userIDs) > 0 {
type scanItem struct {
ID uint64
Username string
IsAdmin bool
}
var scan = []scanItem{}
if res := DB.Table(
"users",
).Select(
"id",
"username",
"is_admin",
).Where(
"id IN ?", userIDs,
).Scan(&scan); res.Error != nil {
return nil, fmt.Errorf("GetBlocklistInsights(%s): mapping user IDs to names: %s", user.Username, res.Error)
}
for _, row := range scan {
usernames[row.ID] = row.Username
admins[row.ID] = row.IsAdmin
}
}
// Assemble the final result.
var result = &BlocklistInsight{
Blocks: []BlocklistInsightUser{},
BlockedBy: []BlocklistInsightUser{},
}
for _, row := range forward {
if username, ok := usernames[row.TargetUserID]; ok {
result.Blocks = append(result.Blocks, BlocklistInsightUser{
Username: username,
IsAdmin: admins[row.TargetUserID],
Date: row.CreatedAt,
})
}
}
for _, row := range reverse {
if username, ok := usernames[row.SourceUserID]; ok {
result.BlockedBy = append(result.BlockedBy, BlocklistInsightUser{
Username: username,
IsAdmin: admins[row.SourceUserID],
Date: row.CreatedAt,
})
}
}
return result, nil
}
type BlocklistInsight struct {
Blocks []BlocklistInsightUser
BlockedBy []BlocklistInsightUser
}
type BlocklistInsightUser struct {
Username string
IsAdmin bool
Date time.Time
}
// UnblockUser removes targetUserID from your blocklist.
func UnblockUser(sourceUserID, targetUserID uint64) error {
result := DB.Where(
"source_user_id = ? AND target_user_id = ?",
sourceUserID, targetUserID,
).Delete(&Block{})
return result.Error
}
// Save photo.
func (b *Block) Save() error {
result := DB.Save(b)
return result.Error
}