Files
nostrlib/eventstore/mmm/stats.go
2025-12-18 13:26:09 -03:00

178 lines
4.2 KiB
Go

package mmm
import (
"bytes"
"encoding/binary"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/eventstore/codec/betterbinary"
"github.com/PowerDNS/lmdb-go/lmdb"
)
type EventStats struct {
Total uint
PerWeek []uint
PerPubKey map[nostr.PubKey]PubKeyStats
PerKind map[nostr.Kind]KindStats
}
type KindStats struct {
Total uint
PerWeek []uint
}
type PubKeyStats struct {
Total uint
PerWeek []uint
PerKind map[nostr.Kind]uint
PerKindPerWeek map[nostr.Kind][]uint
}
type StatsOptions struct {
OnlyPubKey nostr.PubKey
}
func (il *IndexingLayer) ComputeStats(opts StatsOptions) (EventStats, error) {
stats := EventStats{
Total: 0,
PerWeek: make([]uint, 0, 24),
PerPubKey: make(map[nostr.PubKey]PubKeyStats, 30),
PerKind: make(map[nostr.Kind]KindStats, 20),
}
err := il.lmdbEnv.View(func(txn *lmdb.Txn) error {
cursor, err := txn.OpenCursor(il.indexPubkeyKind)
if err != nil {
return err
}
defer cursor.Close()
var currentPubKeyPrefix []byte
var currentPubKey nostr.PubKey
// position cursor based on options
var initialKey []byte
if opts.OnlyPubKey != nostr.ZeroPK {
// position cursor at the start of this author's data
initialKey = make([]byte, 8+4+4)
copy(initialKey[0:8], opts.OnlyPubKey[0:8])
}
var key []byte
var val []byte
if initialKey == nil {
key, val, err = cursor.Get(nil, nil, lmdb.Next)
if lmdb.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
} else {
key, val, err = cursor.Get(initialKey, nil, lmdb.SetRange)
if err != nil {
return err
}
}
for {
// parse key: [8 bytes pubkey][2 bytes kind][4 bytes timestamp]
pubkeyPrefix := key[0:8]
if !bytes.Equal(pubkeyPrefix, currentPubKeyPrefix) {
if opts.OnlyPubKey != nostr.ZeroPK && len(currentPubKeyPrefix) > 0 {
// stop scanning now as we're filtering for a specific pubkey
break
}
// load pubkey from event (otherwise will use the same from before)
pos := positionFromBytes(val)
currentPubKey = betterbinary.GetPubKey(il.mmmm.mmapf[pos.start : pos.start+uint64(pos.size)])
currentPubKeyPrefix = pubkeyPrefix
}
kind := nostr.Kind(binary.BigEndian.Uint16(key[8:10]))
createdTime := time.Unix(int64(binary.BigEndian.Uint32(key[10:14])), 0)
// figure out how many weeks in the past this is
weekIndex := weeksInPast(createdTime)
// update totals
stats.Total++
if weekIndex >= 0 {
for len(stats.PerWeek) <= weekIndex {
stats.PerWeek = append(stats.PerWeek, 0)
}
stats.PerWeek[weekIndex]++
}
if this, exists := stats.PerPubKey[currentPubKey]; exists {
this.Total++
this.PerKind[kind]++
if weekIndex >= 0 {
for len(this.PerWeek) <= weekIndex {
this.PerWeek = append(this.PerWeek, 0)
}
this.PerWeek[weekIndex]++
}
stats.PerPubKey[currentPubKey] = this
} else {
stats.PerPubKey[currentPubKey] = PubKeyStats{
Total: 1,
PerKind: map[nostr.Kind]uint{
kind: 1,
},
}
}
if this, exists := stats.PerKind[kind]; exists {
this.Total++
if weekIndex >= 0 {
for len(this.PerWeek) <= weekIndex {
this.PerWeek = append(this.PerWeek, 0)
}
this.PerWeek[weekIndex]++
}
stats.PerKind[kind] = this
} else {
stats.PerKind[kind] = KindStats{
Total: 1,
}
}
key, val, err = cursor.Get(nil, nil, lmdb.Next)
if lmdb.IsNotFound(err) {
break
}
if err != nil {
return err
}
}
return nil
})
return stats, err
}
func weeksInPast(date time.Time) int {
now := time.Now()
if date.After(now) {
// when in the future always return -1
return -1
}
lastSaturday := now.AddDate(0, 0, -int(now.Weekday()+1))
lastSaturday = time.Date(lastSaturday.Year(), lastSaturday.Month(), lastSaturday.Day(), 23, 59, 59, 0, lastSaturday.Location())
// if the date is after the last completed Saturday, it's in the current incomplete week
if date.After(lastSaturday) {
return 0
}
// calculate the number of complete weeks between the date and last saturday
daysDiff := int(lastSaturday.Sub(date).Hours() / 24)
completeWeeks := (daysDiff / 7) + 1 // +1 because we've already passed at least one Saturday
return completeWeeks
}