implemented reactions module

This commit is contained in:
d1nch8g
2026-05-11 12:07:03 +07:00
parent 6032acdcd7
commit 83627be743
4 changed files with 553 additions and 350 deletions
+19 -11
View File
@@ -146,12 +146,26 @@ type Reactions interface {
SetAllowed(author string, date time.Time, emojis []string) error
Allowed(author string, date time.Time) ([]string, error)
AddExternal(user, domain, author string, date time.Time, emoji string, sig []byte) error
RemoveExternal(user, domain, author string, date time.Time, emoji string) error
ToExternal(domain, author string, date time.Time) ([]Reaction, error)
Add(user, author, domain string, date time.Time, emoji string, sig []byte) error
Remove(user, author, domain string, date time.Time, emoji string) error
OfUser(user string, limit, offset int) ([]ReactionToForeign, error)
ToContent(author, domain string, date time.Time, limit, offset int) ([]ReactionOfDomestic, error)
UpdateReference(author string, date time.Time, domain, emoji string, delta int) error
ListReactions(author string, date time.Time) ([]ReactionCounter, error)
UpdateReference(owner string, date time.Time, domain, emoji string, delta int) error
ListReactions(owner string, date time.Time) ([]ReactionCounter, error)
}
type ReactionToForeign struct {
Domain string `json:"domain"`
Author string `json:"author"`
Date time.Time `json:"date"`
Emoji string `json:"emoji"`
}
type ReactionOfDomestic struct {
User string `json:"user"`
Emoji string `json:"emoji"`
Signature []byte `json:"signature"`
}
type ReactionCounter struct {
@@ -159,12 +173,6 @@ type ReactionCounter struct {
Count uint64 `json:"count"`
}
type Reaction struct {
User string `json:"user"`
Emoji string `json:"emoji"`
Signature []byte `json:"signature"`
}
// type Comments interface {
// Enable(typ string, id uuid.UUID) error
// Disable(typ string, id uuid.UUID) error
+3 -4
View File
@@ -44,7 +44,7 @@ type DB struct {
email *email
chat *chat
posts *posts
// reactions *reactions
reactions *reactions
// comments *comments
}
@@ -63,7 +63,7 @@ func New(path string) (*DB, error) {
email: &email{db: db},
chat: &chat{db: db},
posts: &posts{db: db},
// reactions: &reactions{db: db, flushInterval: time.Second},
reactions: &reactions{db: db, flushInterval: time.Second},
// comments: &comments{db: ldb},
}, nil
}
@@ -106,8 +106,7 @@ func (d *DB) Files() database.Files { return d.files }
func (d *DB) Email() database.Email { return d.email }
func (d *DB) Chat() database.Chat { return d.chat }
func (d *DB) Posts() database.Posts { return d.posts }
// func (d *DB) Reactions() database.Reactions { return d.reactions }
func (d *DB) Reactions() database.Reactions { return d.reactions }
// func (d *DB) Comments() database.Comments { return d.comments }
+217 -160
View File
@@ -1,206 +1,263 @@
package leveldb
// import (
// "bytes"
// "encoding/binary"
// "encoding/gob"
// "fmt"
// "sync"
// "time"
import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"sync"
"time"
// "github.com/d1nch8g/mesh/database"
// "github.com/syndtr/goleveldb/leveldb"
// "github.com/syndtr/goleveldb/leveldb/util"
// )
"github.com/d1nch8g/mesh/database"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
// type reactions struct {
// db *leveldb.DB
// counters sync.Map
// flushInterval time.Duration
// }
type reactions struct {
db *leveldb.DB
counters sync.Map
flushInterval time.Duration
}
// func (r *reactions) SetAllowed(author string, date time.Time, emojis []string) error {
// k := key(author, prefixReactionsAllowed, formatTime(date))
func (r *reactions) SetAllowed(owner string, date time.Time, emojis []string) error {
k := key(owner, prefixReactionsAllowed, formatTime(date))
// var buf bytes.Buffer
// if err := gob.NewEncoder(&buf).Encode(emojis); err != nil {
// return fmt.Errorf("encode allowed reactions for %q: %w", author, err)
// }
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(emojis); err != nil {
return fmt.Errorf("encode allowed reactions for %q: %w", owner, err)
}
// if err := r.db.Put(k, buf.Bytes(), woSync); err != nil {
// return fmt.Errorf("store allowed reactions for %q: %w", author, err)
// }
if err := r.db.Put(k, buf.Bytes(), woSync); err != nil {
return fmt.Errorf("store allowed reactions for %q: %w", owner, err)
}
// return nil
// }
return nil
}
// func (r *reactions) Allowed(author string, date time.Time) ([]string, error) {
// k := key(author, prefixReactionsAllowed, formatTime(date))
func (r *reactions) Allowed(owner string, date time.Time) ([]string, error) {
k := key(owner, prefixReactionsAllowed, formatTime(date))
// data, err := r.db.Get(k, nil)
// if err != nil {
// if err == leveldb.ErrNotFound {
// return nil, database.ErrNotFound
// }
// return nil, fmt.Errorf("read allowed reactions for %q: %w", author, err)
// }
data, err := r.db.Get(k, nil)
if err != nil {
if err == leveldb.ErrNotFound {
return nil, database.ErrNotFound
}
return nil, fmt.Errorf("read allowed reactions for %q: %w", owner, err)
}
// var emojis []string
// if err = gob.NewDecoder(bytes.NewReader(data)).Decode(&emojis); err != nil {
// return nil, fmt.Errorf("decode allowed reactions for %q: %w", author, err)
// }
var emojis []string
if err = gob.NewDecoder(bytes.NewReader(data)).Decode(&emojis); err != nil {
return nil, fmt.Errorf("decode allowed reactions for %q: %w", owner, err)
}
// return emojis, nil
// }
return emojis, nil
}
// func (r *reactions) AddExternal(user, domain, author string, date time.Time, emoji string, sig []byte) error {
// domesticKey := key(user, prefixReactionsOut, domain, formatTime(date), emoji)
// foreignKey := key(domain, prefixReactionsIn, user, formatTime(date), emoji)
func (r *reactions) Add(user, author, domain string, date time.Time, emoji string, sig []byte) error {
ts := formatTime(date)
// batch := new(leveldb.Batch)
// defer batch.Reset()
domesticKey := key(user, prefixReactionsOut, author, domain, ts, emoji)
foreignKey := key(domain, prefixReactionsIn, author, ts, user, emoji)
// batch.Put(domesticKey, sig)
// batch.Put(foreignKey, sig)
batch := new(leveldb.Batch)
defer batch.Reset()
// if err := r.db.Write(batch, woSync); err != nil {
// return fmt.Errorf("store reaction for %q: %w", user, err)
// }
batch.Put(domesticKey, sig)
batch.Put(foreignKey, sig)
// return nil
// }
if err := r.db.Write(batch, woSync); err != nil {
return fmt.Errorf("store reaction for %q: %w", user, err)
}
// func (r *reactions) RemoveExternal(user, domain, author string, date time.Time, emoji string) error {
// domesticKey := key(user, prefixReactionsOut, domain, formatTime(date), emoji)
// foreignKey := key(domain, prefixReactionsIn, user, formatTime(date), emoji)
return nil
}
// _, err := r.db.Get(domesticKey, nil)
// if err != nil {
// if err == leveldb.ErrNotFound {
// return database.ErrNotFound
// }
// return fmt.Errorf("check reaction for %q: %w", user, err)
// }
func (r *reactions) Remove(user, author, domain string, date time.Time, emoji string) error {
ts := formatTime(date)
// batch := new(leveldb.Batch)
// defer batch.Reset()
domesticKey := key(user, prefixReactionsOut, author, domain, ts, emoji)
foreignKey := key(domain, prefixReactionsIn, author, ts, user, emoji)
// batch.Delete(domesticKey)
// batch.Delete(foreignKey)
_, err := r.db.Get(domesticKey, nil)
if err != nil {
if err == leveldb.ErrNotFound {
return database.ErrNotFound
}
return fmt.Errorf("check reaction for %q: %w", user, err)
}
// if err := r.db.Write(batch, woSync); err != nil {
// return fmt.Errorf("delete reaction for %q: %w", user, err)
// }
batch := new(leveldb.Batch)
defer batch.Reset()
// return nil
// }
batch.Delete(domesticKey)
batch.Delete(foreignKey)
// func (r *reactions) ToExternal(domain, author string, date time.Time) ([]database.Reaction, error) {
// prefix := key(domain, prefixReactionsIn, author, formatTime(date))
if err := r.db.Write(batch, woSync); err != nil {
return fmt.Errorf("delete reaction for %q: %w", user, err)
}
// iter := r.db.NewIterator(util.BytesPrefix(prefix), nil)
// defer iter.Release()
return nil
}
// var reactions []database.Reaction
// for iter.Next() {
// parts := bytes.Split(iter.Key(), []byte(":"))
// emoji := string(parts[len(parts)-1])
func (r *reactions) OfUser(user string, limit, offset int) ([]database.ReactionToForeign, error) {
prefix := key(user, prefixReactionsOut)
// reactions = append(reactions, database.Reaction{
// User: author,
// Emoji: emoji,
// Signature: iter.Value(),
// })
// }
iter := r.db.NewIterator(util.BytesPrefix(prefix), nil)
defer iter.Release()
// return reactions, nil
// }
var reactions []database.ReactionToForeign
skipped := 0
for iter.Next() {
if skipped < offset {
skipped++
continue
}
// func (r *reactions) UpdateReference(author string, date time.Time, domain, emoji string, delta int) error {
// domainKey := key(author, prefixReactionsRef, formatTime(date), "perdomain", domain, emoji)
// totalKey := key(author, prefixReactionsRef, formatTime(date), "total", emoji)
parts := bytes.Split(iter.Key(), []byte(":"))
author := string(parts[len(parts)-4])
domain := string(parts[len(parts)-3])
dateBytes := parts[len(parts)-2]
emoji := string(parts[len(parts)-1])
// r.addHot(domainKey, delta)
// r.addHot(totalKey, delta)
ts := binary.BigEndian.Uint64(dateBytes)
date := time.Unix(0, int64(ts))
// return nil
// }
reactions = append(reactions, database.ReactionToForeign{
Domain: domain,
Author: author,
Date: date,
Emoji: emoji,
})
// func (r *reactions) addHot(k []byte, delta int) {
// cnt := &counter{}
if len(reactions) >= limit {
break
}
}
// entry, ok := r.counters.LoadOrStore(string(k), cnt)
// if !ok {
// cnt.mu.Lock()
// data, err := r.db.Get(k, nil)
// if err == nil && len(data) == 8 {
// cnt.val = binary.LittleEndian.Uint64(data)
// }
// cnt.mu.Unlock()
// } else {
// cnt = entry.(*counter)
// }
return reactions, nil
}
// cnt.mu.Lock()
// val := int64(cnt.val) + int64(delta)
// if val < 0 {
// val = 0
// }
// cnt.val = uint64(val)
func (r *reactions) ToContent(author, domain string, date time.Time, limit, offset int) ([]database.ReactionOfDomestic, error) {
prefix := key(domain, prefixReactionsIn, author, formatTime(date))
// if !cnt.jobRunning {
// cnt.jobRunning = true
// go r.flushCounter(k, cnt)
// }
// cnt.mu.Unlock()
// }
iter := r.db.NewIterator(util.BytesPrefix(prefix), nil)
defer iter.Release()
// func (r *reactions) flushCounter(k []byte, ctrl *counter) {
// time.Sleep(r.flushInterval)
var reactions []database.ReactionOfDomestic
skipped := 0
for iter.Next() {
if skipped < offset {
skipped++
continue
}
// ctrl.mu.Lock()
// defer ctrl.mu.Unlock()
parts := bytes.Split(iter.Key(), []byte(":"))
user := string(parts[len(parts)-2])
emoji := string(parts[len(parts)-1])
// if ctrl.val == 0 {
// r.db.Delete(k, woSync)
// } else {
// var buf [8]byte
// binary.LittleEndian.PutUint64(buf[:], ctrl.val)
// r.db.Put(k, buf[:], woSync)
// ctrl.val = 0
// }
sig := make([]byte, len(iter.Value()))
copy(sig, iter.Value())
// ctrl.jobRunning = false
// }
reactions = append(reactions, database.ReactionOfDomestic{
User: user,
Emoji: emoji,
Signature: sig,
})
// func (r *reactions) ListReactions(author string, date time.Time) ([]database.ReactionCounter, error) {
// prefix := key(author, prefixReactionsRef, formatTime(date), "total")
if len(reactions) >= limit {
break
}
}
// iter := r.db.NewIterator(util.BytesPrefix(prefix), nil)
// defer iter.Release()
return reactions, nil
}
// var result []database.ReactionCounter
// for iter.Next() {
// parts := bytes.Split(iter.Key(), []byte(":"))
// emoji := string(parts[len(parts)-1])
func (r *reactions) UpdateReference(owner string, date time.Time, domain, emoji string, delta int) error {
ts := formatTime(date)
domainKey := key(owner, prefixReactionsRef, ts, "perdomain", domain, emoji)
totalKey := key(owner, prefixReactionsRef, ts, "total", emoji)
// diskVal := binary.LittleEndian.Uint64(iter.Value())
// count := diskVal
r.addHot(domainKey, delta)
r.addHot(totalKey, delta)
// if entry, ok := r.counters.Load(string(iter.Key())); ok {
// cnt := entry.(*counter)
// cnt.mu.RLock()
// count += cnt.val
// cnt.mu.RUnlock()
// }
return nil
}
// if count > 0 {
// result = append(result, database.ReactionCounter{
// Emoji: emoji,
// Count: count,
// })
// }
// }
func (r *reactions) addHot(k []byte, delta int) {
cnt := &counter{}
// return result, nil
// }
entry, ok := r.counters.LoadOrStore(string(k), cnt)
if !ok {
cnt.mu.Lock()
data, err := r.db.Get(k, nil)
if err == nil && len(data) == 8 {
cnt.val = binary.LittleEndian.Uint64(data)
}
cnt.mu.Unlock()
} else {
cnt = entry.(*counter)
}
cnt.mu.Lock()
val := int64(cnt.val) + int64(delta)
if val < 0 {
val = 0
}
cnt.val = uint64(val)
if !cnt.jobRunning {
cnt.jobRunning = true
go r.flushCounter(k, cnt)
}
cnt.mu.Unlock()
}
func (r *reactions) flushCounter(k []byte, ctrl *counter) {
time.Sleep(r.flushInterval)
ctrl.mu.Lock()
defer ctrl.mu.Unlock()
if ctrl.val == 0 {
r.db.Delete(k, woSync)
} else {
var buf [8]byte
binary.LittleEndian.PutUint64(buf[:], ctrl.val)
r.db.Put(k, buf[:], woSync)
ctrl.val = 0
}
ctrl.jobRunning = false
}
func (r *reactions) ListReactions(owner string, date time.Time) ([]database.ReactionCounter, error) {
prefix := key(owner, prefixReactionsRef, formatTime(date), "total")
iter := r.db.NewIterator(util.BytesPrefix(prefix), nil)
defer iter.Release()
var result []database.ReactionCounter
for iter.Next() {
parts := bytes.Split(iter.Key(), []byte(":"))
emoji := string(parts[len(parts)-1])
diskVal := binary.LittleEndian.Uint64(iter.Value())
count := diskVal
if entry, ok := r.counters.Load(string(iter.Key())); ok {
cnt := entry.(*counter)
cnt.mu.RLock()
count += cnt.val
cnt.mu.RUnlock()
}
if count > 0 {
result = append(result, database.ReactionCounter{
Emoji: emoji,
Count: count,
})
}
}
return result, nil
}
+314 -175
View File
@@ -1,220 +1,359 @@
package leveldb
// import (
// "testing"
// "time"
import (
"testing"
"time"
// "github.com/d1nch8g/mesh/database"
// "github.com/stretchr/testify/require"
// )
"github.com/d1nch8g/mesh/database"
"github.com/stretchr/testify/require"
)
// func TestReactions_SetAllowed(t *testing.T) {
// db, cleanup := openDB(t)
// defer cleanup()
func TestReactions_SetAllowed(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
// reactions := db.Reactions()
// author := "masha@d1.com"
// date := time.Now()
reactions := db.Reactions()
owner := "masha"
date := time.Now()
// t.Run("set allowed emojis", func(t *testing.T) {
// err := reactions.SetAllowed(author, date, []string{"👍", "❤️", "🔥"})
// require.NoError(t, err)
// })
t.Run("set allowed emojis", func(t *testing.T) {
err := reactions.SetAllowed(owner, date, []string{"👍", "❤️", "🔥"})
require.NoError(t, err)
})
// t.Run("overwrite allowed emojis", func(t *testing.T) {
// err := reactions.SetAllowed(author, date, []string{"👎"})
// require.NoError(t, err)
// })
t.Run("overwrite allowed emojis", func(t *testing.T) {
err := reactions.SetAllowed(owner, date, []string{"👎"})
require.NoError(t, err)
})
// t.Run("set empty allowed", func(t *testing.T) {
// err := reactions.SetAllowed(author, date.Add(time.Second), []string{})
// require.NoError(t, err)
// })
// }
t.Run("set empty allowed", func(t *testing.T) {
err := reactions.SetAllowed(owner, date.Add(time.Second), []string{})
require.NoError(t, err)
})
}
// func TestReactions_Allowed(t *testing.T) {
// db, cleanup := openDB(t)
// defer cleanup()
func TestReactions_Allowed(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
// reactions := db.Reactions()
// author := "masha@d1.com"
// date := time.Now()
reactions := db.Reactions()
owner := "masha"
date := time.Now()
// err := reactions.SetAllowed(author, date, []string{"👍", "🔥"})
// require.NoError(t, err)
err := reactions.SetAllowed(owner, date, []string{"👍", "🔥"})
require.NoError(t, err)
// t.Run("get allowed emojis", func(t *testing.T) {
// got, err := reactions.Allowed(author, date)
// require.NoError(t, err)
// require.Equal(t, []string{"👍", "🔥"}, got)
// })
t.Run("get allowed emojis", func(t *testing.T) {
got, err := reactions.Allowed(owner, date)
require.NoError(t, err)
require.Equal(t, []string{"👍", "🔥"}, got)
})
// t.Run("get non-existing allowed", func(t *testing.T) {
// _, err := reactions.Allowed("unknown@d5.io", date)
// require.ErrorIs(t, err, database.ErrNotFound)
// })
// }
t.Run("get non-existing allowed", func(t *testing.T) {
_, err := reactions.Allowed("unknown", date)
require.ErrorIs(t, err, database.ErrNotFound)
})
// func TestReactions_AddExternal(t *testing.T) {
// db, cleanup := openDB(t)
// defer cleanup()
t.Run("get allowed for different time", func(t *testing.T) {
_, err := reactions.Allowed(owner, date.Add(time.Hour))
require.ErrorIs(t, err, database.ErrNotFound)
})
}
// reactions := db.Reactions()
// user := "masha@d1.com"
// author := "bob@d2.io"
// date := time.Now()
func TestReactions_Add(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
// t.Run("add external reaction", func(t *testing.T) {
// err := reactions.AddExternal(user, "d1.com", author, date, "👍", []byte("sig123"))
// require.NoError(t, err)
// })
reactions := db.Reactions()
user := "masha"
author := "bob"
domain := "d2.io"
date := time.Now()
// t.Run("add another emoji", func(t *testing.T) {
// err := reactions.AddExternal(user, "d1.com", author, date, "🔥", []byte("sig456"))
// require.NoError(t, err)
// })
t.Run("add reaction", func(t *testing.T) {
err := reactions.Add(user, author, domain, date, "👍", []byte("sig123"))
require.NoError(t, err)
})
// t.Run("add with empty signature", func(t *testing.T) {
// err := reactions.AddExternal(user, "d1.com", author, date.Add(time.Second), "❤️", nil)
// require.NoError(t, err)
// })
// }
t.Run("add another emoji", func(t *testing.T) {
err := reactions.Add(user, author, domain, date, "🔥", []byte("sig456"))
require.NoError(t, err)
})
// func TestReactions_RemoveExternal(t *testing.T) {
// db, cleanup := openDB(t)
// defer cleanup()
t.Run("add different user", func(t *testing.T) {
err := reactions.Add("petya", author, domain, date, "👍", []byte("sig789"))
require.NoError(t, err)
})
// reactions := db.Reactions()
// user := "masha@d1.com"
// author := "bob@d2.io"
// date := time.Now()
t.Run("add different content", func(t *testing.T) {
err := reactions.Add(user, "alice", "d3.net", date.Add(time.Second), "❤️", []byte("sig000"))
require.NoError(t, err)
})
// err := reactions.AddExternal(user, "d1.com", author, date, "👍", []byte("sig"))
// require.NoError(t, err)
t.Run("add with empty signature", func(t *testing.T) {
err := reactions.Add(user, author, domain, date.Add(2*time.Second), "❤️", nil)
require.NoError(t, err)
})
}
// t.Run("remove existing reaction", func(t *testing.T) {
// err := reactions.RemoveExternal(user, "d1.com", author, date, "👍")
// require.NoError(t, err)
// })
func TestReactions_Remove(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
// t.Run("remove non-existing returns ErrNotFound", func(t *testing.T) {
// err := reactions.RemoveExternal(user, "d1.com", author, date, "🔥")
// require.ErrorIs(t, err, database.ErrNotFound)
// })
// }
reactions := db.Reactions()
user := "masha"
author := "bob"
domain := "d2.io"
date := time.Now()
// func TestReactions_ToExternal(t *testing.T) {
// db, cleanup := openDB(t)
// defer cleanup()
err := reactions.Add(user, author, domain, date, "👍", []byte("sig"))
require.NoError(t, err)
// reactions := db.Reactions()
// user := "masha@d1.com"
// author := "bob@d2.io"
// date := time.Now()
t.Run("remove existing reaction", func(t *testing.T) {
err := reactions.Remove(user, author, domain, date, "👍")
require.NoError(t, err)
})
// require.NoError(t, reactions.AddExternal(user, "d1.com", author, date, "👍", []byte("sig1")))
// require.NoError(t, reactions.AddExternal(user, "d1.com", author, date, "🔥", []byte("sig2")))
t.Run("remove non-existing returns ErrNotFound", func(t *testing.T) {
err := reactions.Remove(user, author, domain, date, "🔥")
require.ErrorIs(t, err, database.ErrNotFound)
})
// t.Run("list external reactions", func(t *testing.T) {
// list, err := reactions.ToExternal("d1.com", author, date)
// require.NoError(t, err)
// require.Len(t, list, 2)
// })
t.Run("remove twice returns ErrNotFound", func(t *testing.T) {
err := reactions.Remove(user, author, domain, date, "👍")
require.ErrorIs(t, err, database.ErrNotFound)
})
}
// t.Run("empty external reactions", func(t *testing.T) {
// list, err := reactions.ToExternal("unknown.com", author, date)
// require.NoError(t, err)
// require.Empty(t, list)
// })
// }
func TestReactions_OfUser(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
// func TestReactions_UpdateReference(t *testing.T) {
// db, cleanup := openDB(t)
// defer cleanup()
// db.reactions.flushInterval = 10 * time.Millisecond
reactions := db.Reactions()
user := "masha"
date := time.Now()
// reactions := db.Reactions()
// author := "bob@d2.io"
// date := time.Now()
t.Run("empty reactions", func(t *testing.T) {
list, err := reactions.OfUser("newuser", 10, 0)
require.NoError(t, err)
require.Empty(t, list)
})
// t.Run("increment new reaction", func(t *testing.T) {
// err := reactions.UpdateReference(author, date, "d1.com", "👍", 1)
// require.NoError(t, err)
// })
t.Run("single reaction", func(t *testing.T) {
err := reactions.Add(user, "bob", "d2.io", date, "👍", []byte("sig123"))
require.NoError(t, err)
// t.Run("increment existing multiple times", func(t *testing.T) {
// require.NoError(t, reactions.UpdateReference(author, date, "d1.com", "👍", 2))
// require.NoError(t, reactions.UpdateReference(author, date, "d1.com", "👍", 3))
// })
list, err := reactions.OfUser(user, 10, 0)
require.NoError(t, err)
require.Len(t, list, 1)
require.Equal(t, "bob", list[0].Author)
require.Equal(t, "d2.io", list[0].Domain)
require.Equal(t, "👍", list[0].Emoji)
})
// t.Run("decrement partial", func(t *testing.T) {
// require.NoError(t, reactions.UpdateReference(author, date, "d1.com", "👍", -2))
// })
// }
t.Run("multiple reactions", func(t *testing.T) {
err := reactions.Add(user, "alice", "d3.net", date.Add(time.Second), "🔥", []byte("sig456"))
require.NoError(t, err)
// func TestReactions_ListReactions(t *testing.T) {
// db, cleanup := openDB(t)
// defer cleanup()
// db.reactions.flushInterval = 10 * time.Millisecond
list, err := reactions.OfUser(user, 10, 0)
require.NoError(t, err)
require.Len(t, list, 2)
})
// reactions := db.Reactions()
// author := "bob@d2.io"
// date := time.Now()
t.Run("pagination limit", func(t *testing.T) {
list, err := reactions.OfUser(user, 1, 0)
require.NoError(t, err)
require.Len(t, list, 1)
})
// t.Run("empty reactions", func(t *testing.T) {
// list, err := reactions.ListReactions(author, date)
// require.NoError(t, err)
// require.Empty(t, list)
// })
t.Run("pagination offset", func(t *testing.T) {
list, err := reactions.OfUser(user, 10, 1)
require.NoError(t, err)
require.Len(t, list, 1)
})
// t.Run("single emoji after flush", func(t *testing.T) {
// require.NoError(t, reactions.UpdateReference(author, date, "d1.com", "👍", 5))
// time.Sleep(50 * time.Millisecond)
t.Run("user isolation", func(t *testing.T) {
err := reactions.Add("petya", "bob", "d2.io", date, "👍", []byte("sig777"))
require.NoError(t, err)
// list, err := reactions.ListReactions(author, date)
// require.NoError(t, err)
// require.Len(t, list, 1)
// require.Equal(t, "👍", list[0].Emoji)
// require.Equal(t, uint64(5), list[0].Count)
// })
mashaList, err := reactions.OfUser("masha", 10, 0)
require.NoError(t, err)
// t.Run("multiple emojis from different domains", func(t *testing.T) {
// require.NoError(t, reactions.UpdateReference(author, date, "d1.com", "🔥", 3))
// require.NoError(t, reactions.UpdateReference(author, date, "d2.io", "🔥", 2))
// require.NoError(t, reactions.UpdateReference(author, date, "d1.com", "❤️", 1))
// time.Sleep(50 * time.Millisecond)
petyaList, err := reactions.OfUser("petya", 10, 0)
require.NoError(t, err)
// list, err := reactions.ListReactions(author, date)
// require.NoError(t, err)
require.Len(t, mashaList, 2)
require.Len(t, petyaList, 1)
})
}
// counts := make(map[string]uint64)
// for _, r := range list {
// counts[r.Emoji] = r.Count
// }
// require.Equal(t, uint64(5), counts["👍"])
// require.Equal(t, uint64(5), counts["🔥"])
// require.Equal(t, uint64(1), counts["❤️"])
// })
// }
func TestReactions_ToContent(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
// func TestReactions_AfterClose(t *testing.T) {
// path := t.TempDir() + "/db"
// db, err := New(path)
// require.NoError(t, err)
r := db.Reactions()
author := "bob"
domain := "d2.io"
date := time.Now()
// reactions := db.Reactions()
// err = reactions.SetAllowed("masha@d1.com", time.Now(), []string{"👍"})
// require.NoError(t, err)
t.Run("empty reactions", func(t *testing.T) {
list, err := r.ToContent(author, domain, date, 10, 0)
require.NoError(t, err)
require.Empty(t, list)
})
// require.NoError(t, db.Close())
t.Run("reactions from multiple users", func(t *testing.T) {
require.NoError(t, r.Add("masha", author, domain, date, "👍", []byte("sig1")))
require.NoError(t, r.Add("petya", author, domain, date, "🔥", []byte("sig2")))
require.NoError(t, r.Add("alice", author, domain, date, "👍", []byte("sig3")))
// require.NotPanics(t, func() {
// reactions.SetAllowed("masha@d1.com", time.Now(), []string{"👍"})
// reactions.Allowed("masha@d1.com", time.Now())
// reactions.AddExternal("masha@d1.com", "d1.com", "bob@d2.io", time.Now(), "👍", nil)
// reactions.RemoveExternal("masha@d1.com", "d1.com", "bob@d2.io", time.Now(), "👍")
// reactions.ToExternal("d1.com", "bob@d2.io", time.Now())
// reactions.UpdateReference("bob@d2.io", time.Now(), "d1.com", "👍", 1)
// reactions.ListReactions("bob@d2.io", time.Now())
// })
// }
list, err := r.ToContent(author, domain, date, 10, 0)
require.NoError(t, err)
require.Len(t, list, 3)
})
t.Run("pagination", func(t *testing.T) {
list, err := r.ToContent(author, domain, date, 2, 0)
require.NoError(t, err)
require.Len(t, list, 2)
list, err = r.ToContent(author, domain, date, 10, 2)
require.NoError(t, err)
require.Len(t, list, 1)
})
t.Run("wrong domain returns empty", func(t *testing.T) {
list, err := r.ToContent(author, "unknown.com", date, 10, 0)
require.NoError(t, err)
require.Empty(t, list)
})
t.Run("wrong author returns empty", func(t *testing.T) {
list, err := r.ToContent("unknown", domain, date, 10, 0)
require.NoError(t, err)
require.Empty(t, list)
})
t.Run("signature preserved", func(t *testing.T) {
list, err := r.ToContent(author, domain, date, 10, 0)
require.NoError(t, err)
require.Len(t, list, 3)
sigs := make(map[string][]byte)
for _, r := range list {
sigs[r.User] = r.Signature
}
require.Equal(t, []byte("sig1"), sigs["masha"])
require.Equal(t, []byte("sig2"), sigs["petya"])
require.Equal(t, []byte("sig3"), sigs["alice"])
})
}
func TestReactions_UpdateReference(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
db.reactions.flushInterval = 10 * time.Millisecond
reactions := db.Reactions()
owner := "bob"
date := time.Now()
t.Run("increment new reaction", func(t *testing.T) {
err := reactions.UpdateReference(owner, date, "d1.com", "👍", 1)
require.NoError(t, err)
})
t.Run("increment existing multiple times", func(t *testing.T) {
require.NoError(t, reactions.UpdateReference(owner, date, "d1.com", "👍", 2))
require.NoError(t, reactions.UpdateReference(owner, date, "d1.com", "👍", 3))
})
t.Run("decrement partial", func(t *testing.T) {
require.NoError(t, reactions.UpdateReference(owner, date, "d1.com", "👍", -2))
})
t.Run("decrement below zero clamps to zero", func(t *testing.T) {
require.NoError(t, reactions.UpdateReference(owner, date, "d3.net", "🔥", 1))
require.NoError(t, reactions.UpdateReference(owner, date, "d3.net", "🔥", -10))
})
}
func TestReactions_ListReactions(t *testing.T) {
db, cleanup := openDB(t)
defer cleanup()
db.reactions.flushInterval = 10 * time.Millisecond
reactions := db.Reactions()
owner := "bob"
date := time.Now()
t.Run("empty reactions", func(t *testing.T) {
list, err := reactions.ListReactions(owner, date)
require.NoError(t, err)
require.Empty(t, list)
})
t.Run("single emoji after flush", func(t *testing.T) {
require.NoError(t, reactions.UpdateReference(owner, date, "d1.com", "👍", 5))
time.Sleep(50 * time.Millisecond)
list, err := reactions.ListReactions(owner, date)
require.NoError(t, err)
require.Len(t, list, 1)
require.Equal(t, "👍", list[0].Emoji)
require.Equal(t, uint64(5), list[0].Count)
})
t.Run("multiple emojis from different domains", func(t *testing.T) {
require.NoError(t, reactions.UpdateReference(owner, date, "d1.com", "🔥", 3))
require.NoError(t, reactions.UpdateReference(owner, date, "d2.io", "🔥", 2))
require.NoError(t, reactions.UpdateReference(owner, date, "d1.com", "❤️", 1))
time.Sleep(50 * time.Millisecond)
list, err := reactions.ListReactions(owner, date)
require.NoError(t, err)
counts := make(map[string]uint64)
for _, r := range list {
counts[r.Emoji] = r.Count
}
require.Equal(t, uint64(5), counts["👍"])
require.Equal(t, uint64(5), counts["🔥"])
require.Equal(t, uint64(1), counts["❤️"])
})
t.Run("decrement to zero removes emoji", func(t *testing.T) {
owner2 := "test-delete"
date2 := time.Now()
require.NoError(t, reactions.UpdateReference(owner2, date2, "d1.com", "👍", 1))
require.NoError(t, reactions.UpdateReference(owner2, date2, "d1.com", "👍", -1))
time.Sleep(50 * time.Millisecond)
list, err := reactions.ListReactions(owner2, date2)
require.NoError(t, err)
require.Empty(t, list)
})
}
func TestReactions_AfterClose(t *testing.T) {
path := t.TempDir() + "/db"
db, err := New(path)
require.NoError(t, err)
reactions := db.Reactions()
err = reactions.SetAllowed("masha", time.Now(), []string{"👍"})
require.NoError(t, err)
require.NoError(t, db.Close())
require.NotPanics(t, func() {
reactions.SetAllowed("masha", time.Now(), []string{"👍"})
reactions.Allowed("masha", time.Now())
reactions.Add("masha", "bob", "d2.io", time.Now(), "👍", nil)
reactions.Remove("masha", "bob", "d2.io", time.Now(), "👍")
reactions.OfUser("masha", 10, 0)
reactions.ToContent("bob", "d2.io", time.Now(), 10, 0)
reactions.UpdateReference("bob", time.Now(), "d1.com", "👍", 1)
reactions.ListReactions("bob", time.Now())
})
}