create babble package

This commit is contained in:
Sky Johnson 2025-08-08 23:42:24 -05:00
parent 089246dd25
commit 906042a67e
4 changed files with 1579 additions and 0 deletions

350
internal/babble/babble.go Normal file
View File

@ -0,0 +1,350 @@
package babble
import (
"fmt"
"strings"
"time"
"dk/internal/database"
"zombiezen.com/go/sqlite"
)
// Babble represents a global chat message in the database
type Babble struct {
ID int `json:"id"`
Posted int64 `json:"posted"`
Author string `json:"author"`
Babble string `json:"babble"`
db *database.DB
}
// Find retrieves a babble message by ID
func Find(db *database.DB, id int) (*Babble, error) {
babble := &Babble{db: db}
query := "SELECT id, posted, author, babble FROM babble WHERE id = ?"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
babble.ID = stmt.ColumnInt(0)
babble.Posted = stmt.ColumnInt64(1)
babble.Author = stmt.ColumnText(2)
babble.Babble = stmt.ColumnText(3)
return nil
}, id)
if err != nil {
return nil, fmt.Errorf("failed to find babble: %w", err)
}
if babble.ID == 0 {
return nil, fmt.Errorf("babble with ID %d not found", id)
}
return babble, nil
}
// All retrieves all babble messages ordered by posted time (newest first)
func All(db *database.DB) ([]*Babble, error) {
var babbles []*Babble
query := "SELECT id, posted, author, babble FROM babble ORDER BY posted DESC, id DESC"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
babble := &Babble{
ID: stmt.ColumnInt(0),
Posted: stmt.ColumnInt64(1),
Author: stmt.ColumnText(2),
Babble: stmt.ColumnText(3),
db: db,
}
babbles = append(babbles, babble)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to retrieve all babble: %w", err)
}
return babbles, nil
}
// ByAuthor retrieves babble messages by a specific author
func ByAuthor(db *database.DB, author string) ([]*Babble, error) {
var babbles []*Babble
query := "SELECT id, posted, author, babble FROM babble WHERE LOWER(author) = LOWER(?) ORDER BY posted DESC, id DESC"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
babble := &Babble{
ID: stmt.ColumnInt(0),
Posted: stmt.ColumnInt64(1),
Author: stmt.ColumnText(2),
Babble: stmt.ColumnText(3),
db: db,
}
babbles = append(babbles, babble)
return nil
}, author)
if err != nil {
return nil, fmt.Errorf("failed to retrieve babble by author: %w", err)
}
return babbles, nil
}
// Recent retrieves the most recent babble messages (limited by count)
func Recent(db *database.DB, limit int) ([]*Babble, error) {
var babbles []*Babble
query := "SELECT id, posted, author, babble FROM babble ORDER BY posted DESC, id DESC LIMIT ?"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
babble := &Babble{
ID: stmt.ColumnInt(0),
Posted: stmt.ColumnInt64(1),
Author: stmt.ColumnText(2),
Babble: stmt.ColumnText(3),
db: db,
}
babbles = append(babbles, babble)
return nil
}, limit)
if err != nil {
return nil, fmt.Errorf("failed to retrieve recent babble: %w", err)
}
return babbles, nil
}
// Since retrieves babble messages since a specific timestamp
func Since(db *database.DB, since int64) ([]*Babble, error) {
var babbles []*Babble
query := "SELECT id, posted, author, babble FROM babble WHERE posted >= ? ORDER BY posted DESC, id DESC"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
babble := &Babble{
ID: stmt.ColumnInt(0),
Posted: stmt.ColumnInt64(1),
Author: stmt.ColumnText(2),
Babble: stmt.ColumnText(3),
db: db,
}
babbles = append(babbles, babble)
return nil
}, since)
if err != nil {
return nil, fmt.Errorf("failed to retrieve babble since timestamp: %w", err)
}
return babbles, nil
}
// Between retrieves babble messages between two timestamps (inclusive)
func Between(db *database.DB, start, end int64) ([]*Babble, error) {
var babbles []*Babble
query := "SELECT id, posted, author, babble FROM babble WHERE posted >= ? AND posted <= ? ORDER BY posted DESC, id DESC"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
babble := &Babble{
ID: stmt.ColumnInt(0),
Posted: stmt.ColumnInt64(1),
Author: stmt.ColumnText(2),
Babble: stmt.ColumnText(3),
db: db,
}
babbles = append(babbles, babble)
return nil
}, start, end)
if err != nil {
return nil, fmt.Errorf("failed to retrieve babble between timestamps: %w", err)
}
return babbles, nil
}
// Search retrieves babble messages containing the search term (case-insensitive)
func Search(db *database.DB, term string) ([]*Babble, error) {
var babbles []*Babble
query := "SELECT id, posted, author, babble FROM babble WHERE LOWER(babble) LIKE LOWER(?) ORDER BY posted DESC, id DESC"
searchTerm := "%" + term + "%"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
babble := &Babble{
ID: stmt.ColumnInt(0),
Posted: stmt.ColumnInt64(1),
Author: stmt.ColumnText(2),
Babble: stmt.ColumnText(3),
db: db,
}
babbles = append(babbles, babble)
return nil
}, searchTerm)
if err != nil {
return nil, fmt.Errorf("failed to search babble: %w", err)
}
return babbles, nil
}
// RecentByAuthor retrieves recent messages from a specific author
func RecentByAuthor(db *database.DB, author string, limit int) ([]*Babble, error) {
var babbles []*Babble
query := "SELECT id, posted, author, babble FROM babble WHERE LOWER(author) = LOWER(?) ORDER BY posted DESC, id DESC LIMIT ?"
err := db.Query(query, func(stmt *sqlite.Stmt) error {
babble := &Babble{
ID: stmt.ColumnInt(0),
Posted: stmt.ColumnInt64(1),
Author: stmt.ColumnText(2),
Babble: stmt.ColumnText(3),
db: db,
}
babbles = append(babbles, babble)
return nil
}, author, limit)
if err != nil {
return nil, fmt.Errorf("failed to retrieve recent babble by author: %w", err)
}
return babbles, nil
}
// Save updates an existing babble message in the database
func (b *Babble) Save() error {
if b.ID == 0 {
return fmt.Errorf("cannot save babble without ID")
}
query := `UPDATE babble SET posted = ?, author = ?, babble = ? WHERE id = ?`
return b.db.Exec(query, b.Posted, b.Author, b.Babble, b.ID)
}
// Delete removes the babble message from the database
func (b *Babble) Delete() error {
if b.ID == 0 {
return fmt.Errorf("cannot delete babble without ID")
}
query := "DELETE FROM babble WHERE id = ?"
return b.db.Exec(query, b.ID)
}
// PostedTime returns the posted timestamp as a time.Time
func (b *Babble) PostedTime() time.Time {
return time.Unix(b.Posted, 0)
}
// SetPostedTime sets the posted timestamp from a time.Time
func (b *Babble) SetPostedTime(t time.Time) {
b.Posted = t.Unix()
}
// IsRecent returns true if the babble message was posted within the last hour
func (b *Babble) IsRecent() bool {
return time.Since(b.PostedTime()) < time.Hour
}
// Age returns how long ago the babble message was posted
func (b *Babble) Age() time.Duration {
return time.Since(b.PostedTime())
}
// IsAuthor returns true if the given username is the author of this babble message
func (b *Babble) IsAuthor(username string) bool {
return strings.EqualFold(b.Author, username)
}
// Preview returns a truncated version of the babble for previews
func (b *Babble) Preview(maxLength int) string {
if len(b.Babble) <= maxLength {
return b.Babble
}
if maxLength < 3 {
return b.Babble[:maxLength]
}
return b.Babble[:maxLength-3] + "..."
}
// WordCount returns the number of words in the babble message
func (b *Babble) WordCount() int {
if b.Babble == "" {
return 0
}
// Simple word count by splitting on whitespace
words := 0
inWord := false
for _, char := range b.Babble {
if char == ' ' || char == '\t' || char == '\n' || char == '\r' {
if inWord {
words++
inWord = false
}
} else {
inWord = true
}
}
if inWord {
words++
}
return words
}
// Length returns the character length of the babble message
func (b *Babble) Length() int {
return len(b.Babble)
}
// Contains returns true if the babble message contains the given term (case-insensitive)
func (b *Babble) Contains(term string) bool {
return strings.Contains(strings.ToLower(b.Babble), strings.ToLower(term))
}
// IsEmpty returns true if the babble message is empty or whitespace-only
func (b *Babble) IsEmpty() bool {
return strings.TrimSpace(b.Babble) == ""
}
// IsLongMessage returns true if the message exceeds the typical chat length
func (b *Babble) IsLongMessage(threshold int) bool {
return b.Length() > threshold
}
// GetMentions returns a slice of usernames mentioned in the message (starting with @)
func (b *Babble) GetMentions() []string {
words := strings.Fields(b.Babble)
var mentions []string
for _, word := range words {
if strings.HasPrefix(word, "@") && len(word) > 1 {
// Clean up punctuation from the end
mention := strings.TrimRight(word[1:], ".,!?;:")
if mention != "" {
mentions = append(mentions, mention)
}
}
}
return mentions
}
// HasMention returns true if the message mentions the given username
func (b *Babble) HasMention(username string) bool {
mentions := b.GetMentions()
for _, mention := range mentions {
if strings.EqualFold(mention, username) {
return true
}
}
return false
}

View File

@ -0,0 +1,625 @@
package babble
import (
"os"
"testing"
"time"
"dk/internal/database"
)
func setupTestDB(t *testing.T) *database.DB {
testDB := "test_babble.db"
t.Cleanup(func() {
os.Remove(testDB)
})
db, err := database.Open(testDB)
if err != nil {
t.Fatalf("Failed to open test database: %v", err)
}
// Create babble table
createTable := `CREATE TABLE babble (
id INTEGER PRIMARY KEY AUTOINCREMENT,
posted INTEGER NOT NULL DEFAULT (unixepoch()),
author TEXT NOT NULL DEFAULT '',
babble TEXT NOT NULL DEFAULT ''
)`
if err := db.Exec(createTable); err != nil {
t.Fatalf("Failed to create babble table: %v", err)
}
// Insert test data with specific timestamps for predictable testing
now := time.Now().Unix()
testBabble := `INSERT INTO babble (posted, author, babble) VALUES
(?, 'Alice', 'Hello everyone! Welcome to the game'),
(?, 'Bob', 'Thanks Alice! @Alice this game is great'),
(?, 'Charlie', 'Anyone want to team up for the dungeon?'),
(?, 'Alice', 'I can help @Charlie, let me know'),
(?, 'David', 'Server lag is really bad right now...'),
(?, 'Eve', 'Quick question about spell mechanics')`
timestamps := []interface{}{
now - 3600*6, // 6 hours ago
now - 3600*4, // 4 hours ago
now - 3600*2, // 2 hours ago
now - 3600*1, // 1 hour ago
now - 1800, // 30 minutes ago
now - 300, // 5 minutes ago
}
if err := db.Exec(testBabble, timestamps...); err != nil {
t.Fatalf("Failed to insert test babble: %v", err)
}
return db
}
func TestFind(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test finding existing babble
babble, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find babble: %v", err)
}
if babble.ID != 1 {
t.Errorf("Expected ID 1, got %d", babble.ID)
}
if babble.Author != "Alice" {
t.Errorf("Expected author 'Alice', got '%s'", babble.Author)
}
if babble.Babble != "Hello everyone! Welcome to the game" {
t.Errorf("Expected specific message, got '%s'", babble.Babble)
}
if babble.Posted == 0 {
t.Error("Expected non-zero posted timestamp")
}
// Test finding non-existent babble
_, err = Find(db, 999)
if err == nil {
t.Error("Expected error when finding non-existent babble")
}
}
func TestAll(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
babbles, err := All(db)
if err != nil {
t.Fatalf("Failed to get all babble: %v", err)
}
if len(babbles) != 6 {
t.Errorf("Expected 6 babble messages, got %d", len(babbles))
}
// Check ordering (newest first)
if len(babbles) >= 2 {
if babbles[0].Posted < babbles[1].Posted {
t.Error("Expected babble to be ordered by posted time (newest first)")
}
}
// First message should be the most recent (5 minutes ago)
if babbles[0].Author != "Eve" {
t.Errorf("Expected newest message from Eve, got from '%s'", babbles[0].Author)
}
}
func TestByAuthor(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test messages by Alice
aliceMessages, err := ByAuthor(db, "Alice")
if err != nil {
t.Fatalf("Failed to get babble by author: %v", err)
}
if len(aliceMessages) != 2 {
t.Errorf("Expected 2 messages by Alice, got %d", len(aliceMessages))
}
// Verify all messages are by Alice
for _, message := range aliceMessages {
if message.Author != "Alice" {
t.Errorf("Expected author 'Alice', got '%s'", message.Author)
}
}
// Check ordering (newest first)
if len(aliceMessages) == 2 {
if aliceMessages[0].Babble != "I can help @Charlie, let me know" {
t.Errorf("Expected newest message by Alice first")
}
}
// Test case insensitive search
aliceMessagesLower, err := ByAuthor(db, "alice")
if err != nil {
t.Fatalf("Failed to get babble by lowercase author: %v", err)
}
if len(aliceMessagesLower) != 2 {
t.Errorf("Expected case insensitive search to find 2 messages, got %d", len(aliceMessagesLower))
}
// Test author with no messages
noMessages, err := ByAuthor(db, "NonexistentUser")
if err != nil {
t.Fatalf("Failed to query non-existent author: %v", err)
}
if len(noMessages) != 0 {
t.Errorf("Expected 0 messages by non-existent author, got %d", len(noMessages))
}
}
func TestRecent(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test getting 3 most recent messages
recentMessages, err := Recent(db, 3)
if err != nil {
t.Fatalf("Failed to get recent babble: %v", err)
}
if len(recentMessages) != 3 {
t.Errorf("Expected 3 recent messages, got %d", len(recentMessages))
}
// Check ordering (newest first)
if len(recentMessages) >= 2 {
if recentMessages[0].Posted < recentMessages[1].Posted {
t.Error("Expected recent messages to be ordered newest first")
}
}
// Test getting more messages than exist
allRecentMessages, err := Recent(db, 10)
if err != nil {
t.Fatalf("Failed to get recent babble with high limit: %v", err)
}
if len(allRecentMessages) != 6 {
t.Errorf("Expected 6 messages (all available), got %d", len(allRecentMessages))
}
}
func TestSince(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test messages since 3 hours ago
threeHoursAgo := time.Now().Add(-3 * time.Hour).Unix()
recentMessages, err := Since(db, threeHoursAgo)
if err != nil {
t.Fatalf("Failed to get babble since timestamp: %v", err)
}
// Should get messages from 2 hours ago, 1 hour ago, 30 minutes ago, and 5 minutes ago
expectedCount := 4
if len(recentMessages) != expectedCount {
t.Errorf("Expected %d messages since 3 hours ago, got %d", expectedCount, len(recentMessages))
}
// Verify all messages are since the timestamp
for _, message := range recentMessages {
if message.Posted < threeHoursAgo {
t.Errorf("Message with timestamp %d is before the 'since' timestamp %d", message.Posted, threeHoursAgo)
}
}
// Test with future timestamp (should return no messages)
futureMessages, err := Since(db, time.Now().Add(time.Hour).Unix())
if err != nil {
t.Fatalf("Failed to query future timestamp: %v", err)
}
if len(futureMessages) != 0 {
t.Errorf("Expected 0 messages since future timestamp, got %d", len(futureMessages))
}
}
func TestBetween(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test messages between 5 hours ago and 1 hour ago
start := time.Now().Add(-5 * time.Hour).Unix()
end := time.Now().Add(-1 * time.Hour).Unix()
betweenMessages, err := Between(db, start, end)
if err != nil {
t.Fatalf("Failed to get babble between timestamps: %v", err)
}
// Should get messages from 4 hours ago, 2 hours ago, and 1 hour ago (inclusive end)
expectedCount := 3
if len(betweenMessages) != expectedCount {
t.Errorf("Expected %d messages between timestamps, got %d", expectedCount, len(betweenMessages))
}
// Verify all messages are within the range
for _, message := range betweenMessages {
if message.Posted < start || message.Posted > end {
t.Errorf("Message with timestamp %d is outside range [%d, %d]", message.Posted, start, end)
}
}
}
func TestSearch(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test searching for "game"
gameMessages, err := Search(db, "game")
if err != nil {
t.Fatalf("Failed to search babble: %v", err)
}
expectedCount := 2 // Alice's welcome message and Bob's response
if len(gameMessages) != expectedCount {
t.Errorf("Expected %d messages containing 'game', got %d", expectedCount, len(gameMessages))
}
// Verify all messages contain the search term
for _, message := range gameMessages {
if !message.Contains("game") {
t.Errorf("Message '%s' does not contain search term 'game'", message.Babble)
}
}
// Test case insensitive search
gameMessagesUpper, err := Search(db, "GAME")
if err != nil {
t.Fatalf("Failed to search babble with uppercase: %v", err)
}
if len(gameMessagesUpper) != expectedCount {
t.Error("Expected case insensitive search to find same results")
}
// Test search with no results
noResults, err := Search(db, "nonexistentterm")
if err != nil {
t.Fatalf("Failed to search for non-existent term: %v", err)
}
if len(noResults) != 0 {
t.Errorf("Expected 0 results for non-existent term, got %d", len(noResults))
}
}
func TestRecentByAuthor(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test recent messages by Alice (limit 1)
aliceRecent, err := RecentByAuthor(db, "Alice", 1)
if err != nil {
t.Fatalf("Failed to get recent babble by author: %v", err)
}
if len(aliceRecent) != 1 {
t.Errorf("Expected 1 recent message by Alice, got %d", len(aliceRecent))
}
if len(aliceRecent) > 0 && aliceRecent[0].Babble != "I can help @Charlie, let me know" {
t.Error("Expected most recent message by Alice")
}
// Test with higher limit
aliceAll, err := RecentByAuthor(db, "Alice", 5)
if err != nil {
t.Fatalf("Failed to get all recent messages by Alice: %v", err)
}
if len(aliceAll) != 2 {
t.Errorf("Expected 2 total messages by Alice, got %d", len(aliceAll))
}
}
func TestBuilder(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Create new babble using builder
testTime := time.Now()
babble, err := NewBuilder(db).
WithAuthor("TestUser").
WithBabble("Test message from builder").
WithPostedTime(testTime).
Create()
if err != nil {
t.Fatalf("Failed to create babble with builder: %v", err)
}
if babble.ID == 0 {
t.Error("Expected non-zero ID after creation")
}
if babble.Author != "TestUser" {
t.Errorf("Expected author 'TestUser', got '%s'", babble.Author)
}
if babble.Babble != "Test message from builder" {
t.Errorf("Expected specific message, got '%s'", babble.Babble)
}
if babble.Posted != testTime.Unix() {
t.Errorf("Expected posted time %d, got %d", testTime.Unix(), babble.Posted)
}
// Test WithMessage alias
babble2, err := NewBuilder(db).
WithAuthor("TestUser2").
WithMessage("Using WithMessage alias").
Create()
if err != nil {
t.Fatalf("Failed to create babble with WithMessage: %v", err)
}
if babble2.Babble != "Using WithMessage alias" {
t.Errorf("WithMessage alias failed, got '%s'", babble2.Babble)
}
// Verify it was saved to database
foundBabble, err := Find(db, babble.ID)
if err != nil {
t.Fatalf("Failed to find created babble: %v", err)
}
if foundBabble.Babble != "Test message from builder" {
t.Errorf("Created babble not found in database")
}
// Test builder with default timestamp
defaultBabble, err := NewBuilder(db).
WithAuthor("DefaultUser").
WithBabble("Message with default timestamp").
Create()
if err != nil {
t.Fatalf("Failed to create babble with default timestamp: %v", err)
}
// Should have recent timestamp (within last minute)
if time.Since(defaultBabble.PostedTime()) > time.Minute {
t.Error("Expected default timestamp to be recent")
}
}
func TestSave(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
babble, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find babble: %v", err)
}
// Modify babble
babble.Author = "UpdatedAuthor"
babble.Babble = "Updated message content"
babble.Posted = time.Now().Unix()
// Save changes
err = babble.Save()
if err != nil {
t.Fatalf("Failed to save babble: %v", err)
}
// Verify changes were saved
updatedBabble, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find updated babble: %v", err)
}
if updatedBabble.Author != "UpdatedAuthor" {
t.Errorf("Expected updated author 'UpdatedAuthor', got '%s'", updatedBabble.Author)
}
if updatedBabble.Babble != "Updated message content" {
t.Errorf("Expected updated message, got '%s'", updatedBabble.Babble)
}
}
func TestDelete(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
babble, err := Find(db, 1)
if err != nil {
t.Fatalf("Failed to find babble: %v", err)
}
// Delete babble
err = babble.Delete()
if err != nil {
t.Fatalf("Failed to delete babble: %v", err)
}
// Verify babble was deleted
_, err = Find(db, 1)
if err == nil {
t.Error("Expected error when finding deleted babble")
}
}
func TestUtilityMethods(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
babble, _ := Find(db, 1)
// Test PostedTime
postedTime := babble.PostedTime()
if postedTime.IsZero() {
t.Error("Expected non-zero posted time")
}
// Test SetPostedTime
newTime := time.Now().Add(-30 * time.Minute)
babble.SetPostedTime(newTime)
if babble.Posted != newTime.Unix() {
t.Errorf("Expected posted timestamp %d, got %d", newTime.Unix(), babble.Posted)
}
// Test IsRecent (should be true for 30 minutes ago)
if !babble.IsRecent() {
t.Error("Expected message from 30 minutes ago to be recent")
}
// Test Age
age := babble.Age()
if age < 0 {
t.Error("Expected positive age")
}
// Test IsAuthor
if !babble.IsAuthor("Alice") {
t.Error("Expected IsAuthor to return true for correct author")
}
if !babble.IsAuthor("alice") { // Test case insensitive
t.Error("Expected IsAuthor to be case insensitive")
}
if babble.IsAuthor("Bob") {
t.Error("Expected IsAuthor to return false for incorrect author")
}
// Test Preview
longMessage := "This is a very long chat message that should be truncated when preview is called for display purposes"
babble.Babble = longMessage
preview := babble.Preview(20)
if len(preview) > 20 {
t.Errorf("Expected preview length <= 20, got %d", len(preview))
}
if preview[len(preview)-3:] != "..." {
t.Error("Expected preview to end with ellipsis")
}
shortPreview := babble.Preview(200) // Longer than message
if shortPreview != longMessage {
t.Error("Expected short message to not be truncated")
}
// Test WordCount
babble.Babble = "This is a test with five words"
wordCount := babble.WordCount()
if wordCount != 7 {
t.Errorf("Expected 7 words, got %d", wordCount)
}
// Test Length
expectedLength := len(babble.Babble)
if babble.Length() != expectedLength {
t.Errorf("Expected length %d, got %d", expectedLength, babble.Length())
}
// Test Contains
if !babble.Contains("test") {
t.Error("Expected message to contain 'test'")
}
if !babble.Contains("TEST") { // Case insensitive
t.Error("Expected Contains to be case insensitive")
}
if babble.Contains("nonexistent") {
t.Error("Expected message not to contain 'nonexistent'")
}
// Test IsEmpty
babble.Babble = ""
if !babble.IsEmpty() {
t.Error("Expected empty message to be empty")
}
babble.Babble = " "
if !babble.IsEmpty() {
t.Error("Expected whitespace-only message to be empty")
}
babble.Babble = "Not empty"
if babble.IsEmpty() {
t.Error("Expected non-empty message not to be empty")
}
// Test IsLongMessage
shortMsg := "Short"
babble.Babble = shortMsg
if babble.IsLongMessage(100) {
t.Error("Expected short message not to be long")
}
if !babble.IsLongMessage(3) {
t.Error("Expected message longer than threshold to be long")
}
}
func TestMentionMethods(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// Test GetMentions
babble, _ := Find(db, 2) // Bob's message: "Thanks Alice! @Alice this game is great"
mentions := babble.GetMentions()
expectedMentions := []string{"Alice"}
if len(mentions) != len(expectedMentions) {
t.Errorf("Expected %d mentions, got %d", len(expectedMentions), len(mentions))
}
for i, expected := range expectedMentions {
if i < len(mentions) && mentions[i] != expected {
t.Errorf("Expected mention '%s' at position %d, got '%s'", expected, i, mentions[i])
}
}
// Test HasMention
if !babble.HasMention("Alice") {
t.Error("Expected message to mention Alice")
}
if !babble.HasMention("alice") { // Case insensitive
t.Error("Expected HasMention to be case insensitive")
}
if babble.HasMention("Bob") {
t.Error("Expected message not to mention Bob")
}
// Test message with multiple mentions and punctuation
babble.Babble = "Hey @Alice, @Bob! Can you help @Charlie?"
mentions = babble.GetMentions()
expectedMentions = []string{"Alice", "Bob", "Charlie"}
if len(mentions) != len(expectedMentions) {
t.Errorf("Expected %d mentions, got %d: %v", len(expectedMentions), len(mentions), mentions)
}
for _, expected := range expectedMentions {
if !babble.HasMention(expected) {
t.Errorf("Expected message to mention %s", expected)
}
}
// Test message with no mentions
babble.Babble = "No mentions in this message"
mentions = babble.GetMentions()
if len(mentions) != 0 {
t.Errorf("Expected 0 mentions, got %d", len(mentions))
}
// Test malformed mentions (should be ignored)
babble.Babble = "Just @ alone or @"
mentions = babble.GetMentions()
if len(mentions) != 0 {
t.Errorf("Expected 0 mentions for malformed @, got %d", len(mentions))
}
}

View File

@ -0,0 +1,90 @@
package babble
import (
"fmt"
"time"
"dk/internal/database"
"zombiezen.com/go/sqlite"
)
// Builder provides a fluent interface for creating babble messages
type Builder struct {
babble *Babble
db *database.DB
}
// NewBuilder creates a new babble builder
func NewBuilder(db *database.DB) *Builder {
return &Builder{
babble: &Babble{
db: db,
Posted: time.Now().Unix(), // Default to current time
},
db: db,
}
}
// WithAuthor sets the author username
func (b *Builder) WithAuthor(author string) *Builder {
b.babble.Author = author
return b
}
// WithBabble sets the message content
func (b *Builder) WithBabble(message string) *Builder {
b.babble.Babble = message
return b
}
// WithMessage is an alias for WithBabble for more intuitive usage
func (b *Builder) WithMessage(message string) *Builder {
return b.WithBabble(message)
}
// WithPosted sets the posted timestamp
func (b *Builder) WithPosted(posted int64) *Builder {
b.babble.Posted = posted
return b
}
// WithPostedTime sets the posted timestamp from a time.Time
func (b *Builder) WithPostedTime(t time.Time) *Builder {
b.babble.Posted = t.Unix()
return b
}
// Create saves the babble message to the database and returns the created babble with ID
func (b *Builder) Create() (*Babble, error) {
// Use a transaction to ensure we can get the ID
var babble *Babble
err := b.db.Transaction(func(tx *database.Tx) error {
query := `INSERT INTO babble (posted, author, babble)
VALUES (?, ?, ?)`
if err := tx.Exec(query, b.babble.Posted, b.babble.Author, b.babble.Babble); err != nil {
return fmt.Errorf("failed to insert babble: %w", err)
}
// Get the last insert ID
var id int
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
id = stmt.ColumnInt(0)
return nil
})
if err != nil {
return fmt.Errorf("failed to get insert ID: %w", err)
}
b.babble.ID = id
babble = b.babble
return nil
})
if err != nil {
return nil, err
}
return babble, nil
}

514
internal/babble/doc.go Normal file
View File

@ -0,0 +1,514 @@
/*
Package babble is the active record implementation for global chat messages in the game.
Babble represents the global chat system where players can communicate with each other in real-time. The package provides comprehensive chat message management with features like mentions, search, time-based queries, and moderation utilities.
# Basic Usage
To retrieve a babble message by ID:
message, err := babble.Find(db, 1)
if err != nil {
log.Fatal(err)
}
fmt.Printf("<%s> %s\n", message.Author, message.Babble)
To get all babble messages:
allMessages, err := babble.All(db)
if err != nil {
log.Fatal(err)
}
for _, message := range allMessages {
fmt.Printf("[%s] <%s> %s\n",
message.PostedTime().Format("15:04"),
message.Author,
message.Babble)
}
To get recent chat messages:
recentChat, err := babble.Recent(db, 50)
if err != nil {
log.Fatal(err)
}
To filter messages by author:
userMessages, err := babble.ByAuthor(db, "PlayerName")
if err != nil {
log.Fatal(err)
}
# Creating Messages with Builder Pattern
The package provides a fluent builder interface for creating new chat messages:
message, err := babble.NewBuilder(db).
WithAuthor("PlayerName").
WithBabble("Hello everyone! Ready for some adventure?").
WithPostedTime(time.Now()).
Create()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Posted message with ID: %d\n", message.ID)
The builder automatically sets the current time if no posted time is specified:
message, err := babble.NewBuilder(db).
WithAuthor("Admin").
WithMessage("Server restart in 5 minutes!").
Create() // Uses current timestamp
You can use either `WithBabble()` or `WithMessage()` - they are aliases for the same functionality.
# Updating Messages
Chat messages can be modified and saved back to the database:
message, _ := babble.Find(db, 1)
message.Babble = "[EDITED] Original message was inappropriate"
err := message.Save()
if err != nil {
log.Fatal(err)
}
# Deleting Messages
Messages can be removed from the database (for moderation):
message, _ := babble.Find(db, 1)
err := message.Delete()
if err != nil {
log.Fatal(err)
}
# Database Schema
The babble table has the following structure:
CREATE TABLE babble (
id INTEGER PRIMARY KEY AUTOINCREMENT,
posted INTEGER NOT NULL DEFAULT (unixepoch()),
author TEXT NOT NULL DEFAULT '',
babble TEXT NOT NULL DEFAULT ''
)
Where:
- id: Unique identifier for the chat message
- posted: Unix timestamp when the message was posted
- author: Username of the player who posted the message
- babble: The text content of the chat message
# Time-Based Queries
## Recent Messages
Get the most recent chat messages:
// Get 100 most recent messages for chat display
chatHistory, err := babble.Recent(db, 100)
if err != nil {
log.Fatal(err)
}
fmt.Println("=== Recent Chat ===")
for _, msg := range chatHistory {
age := msg.Age()
timeStr := ""
if age < time.Minute {
timeStr = "just now"
} else if age < time.Hour {
timeStr = fmt.Sprintf("%dm ago", int(age.Minutes()))
} else {
timeStr = msg.PostedTime().Format("15:04")
}
fmt.Printf("[%s] <%s> %s\n", timeStr, msg.Author, msg.Babble)
}
## Messages Since Timestamp
Get messages posted since a specific time:
// Get messages since user's last login
lastLogin := getUserLastLogin(userID)
newMessages, err := babble.Since(db, lastLogin)
if err != nil {
log.Fatal(err)
}
if len(newMessages) > 0 {
fmt.Printf("You missed %d messages while you were away\n", len(newMessages))
}
## Messages Between Timestamps
Get messages within a time range:
// Get today's chat history
startOfDay := time.Now().Truncate(24 * time.Hour).Unix()
endOfDay := time.Now().Unix()
todaysChat, err := babble.Between(db, startOfDay, endOfDay)
if err != nil {
log.Fatal(err)
}
# Search and Filtering
## Text Search
Search for messages containing specific terms:
// Search for messages about "boss fight"
bossMessages, err := babble.Search(db, "boss fight")
if err != nil {
log.Fatal(err)
}
for _, msg := range bossMessages {
fmt.Printf("<%s> %s\n", msg.Author, msg.Preview(60))
}
Search is case-insensitive and matches partial words.
## Author-Based Queries
Get messages from specific players:
// Get recent messages from a player
playerRecent, err := babble.RecentByAuthor(db, "PlayerName", 10)
if err != nil {
log.Fatal(err)
}
// Get all messages from a player (for moderation)
allPlayerMessages, err := babble.ByAuthor(db, "ReportedPlayer")
if err != nil {
log.Fatal(err)
}
All author searches are case-insensitive.
# Mention System
## Finding Mentions
The package includes a comprehensive mention system using @username syntax:
message, _ := babble.Find(db, someID)
// Get all mentioned usernames
mentions := message.GetMentions()
for _, username := range mentions {
fmt.Printf("Message mentions: @%s\n", username)
}
// Check if specific user is mentioned
if message.HasMention("PlayerName") {
fmt.Println("You were mentioned in this message!")
}
## Mention Parsing
The mention system handles various formats:
- `@username` - Basic mention
- `@username!` - With punctuation
- `@username,` - In lists
- `@username?` - In questions
Mentions are extracted without the punctuation and are case-insensitive.
## Notification Integration
Use mentions for player notifications:
// Process new messages for mentions
recentMessages, _ := babble.Recent(db, 50)
for _, msg := range recentMessages {
mentions := msg.GetMentions()
for _, mentionedUser := range mentions {
// Send notification to mentioned user
notifyUser(mentionedUser, fmt.Sprintf("%s mentioned you: %s",
msg.Author, msg.Preview(50)))
}
}
# Message Analysis
## Content Analysis
Analyze message content for moderation or statistics:
message, _ := babble.Find(db, someID)
// Basic content metrics
fmt.Printf("Length: %d characters\n", message.Length())
fmt.Printf("Words: %d\n", message.WordCount())
// Content checks
if message.IsEmpty() {
fmt.Println("Empty message detected")
}
if message.IsLongMessage(200) {
fmt.Println("Very long message - possible spam")
}
// Search within message
if message.Contains("inappropriate_term") {
fmt.Println("Message flagged for review")
}
## Time Analysis
Analyze posting patterns:
message, _ := babble.Find(db, someID)
age := message.Age()
fmt.Printf("Message posted %v ago\n", age)
if message.IsRecent() {
fmt.Println("This is a recent message (within 1 hour)")
}
// Format for display
postedTime := message.PostedTime()
if age < 24*time.Hour {
fmt.Printf("Posted at %s\n", postedTime.Format("15:04"))
} else {
fmt.Printf("Posted on %s\n", postedTime.Format("Jan 2 15:04"))
}
# Chat Display Patterns
## Live Chat Feed
Display real-time chat messages:
func displayChatFeed(db *database.DB) {
messages, _ := babble.Recent(db, 50)
fmt.Println("=== Global Chat ===")
for i := len(messages) - 1; i >= 0; i-- { // Reverse for chronological order
msg := messages[i]
// Format timestamp
age := msg.Age()
var timeStr string
if age < time.Minute {
timeStr = "now"
} else if age < time.Hour {
timeStr = fmt.Sprintf("%dm", int(age.Minutes()))
} else {
timeStr = msg.PostedTime().Format("15:04")
}
// Handle mentions highlighting
content := msg.Babble
if msg.HasMention(currentUser) {
content = fmt.Sprintf("🔔 %s", content) // Highlight mentions
}
fmt.Printf("[%s] <%s> %s\n", timeStr, msg.Author, content)
}
}
## Chat History Browser
Browse historical messages:
func browseChatHistory(db *database.DB, page int, pageSize int) {
offset := page * pageSize
// Get paginated results (implement with LIMIT/OFFSET)
allMessages, _ := babble.All(db)
start := offset
end := offset + pageSize
if end > len(allMessages) {
end = len(allMessages)
}
if start >= len(allMessages) {
fmt.Println("No more messages")
return
}
pageMessages := allMessages[start:end]
fmt.Printf("=== Chat History (Page %d) ===\n", page+1)
for _, msg := range pageMessages {
fmt.Printf("%s <%s> %s\n",
msg.PostedTime().Format("Jan 2 15:04"),
msg.Author,
msg.Babble)
}
}
# Moderation Features
## Content Moderation
Tools for chat moderation:
// Flag inappropriate messages
func moderateMessage(db *database.DB, messageID int) {
msg, err := babble.Find(db, messageID)
if err != nil {
return
}
// Check for spam (very short or very long)
if msg.WordCount() < 2 {
fmt.Printf("Possible spam: %s\n", msg.Preview(30))
}
if msg.IsLongMessage(500) {
fmt.Printf("Very long message from %s\n", msg.Author)
}
// Check for excessive mentions
mentions := msg.GetMentions()
if len(mentions) > 5 {
fmt.Printf("Message with %d mentions from %s\n", len(mentions), msg.Author)
}
}
## User Activity Analysis
Analyze user posting patterns:
// Check user activity
func analyzeUserActivity(db *database.DB, username string) {
// Recent activity
recentMessages, _ := babble.RecentByAuthor(db, username, 10)
fmt.Printf("User %s recent activity:\n", username)
fmt.Printf("- Recent messages: %d\n", len(recentMessages))
if len(recentMessages) > 0 {
totalWords := 0
for _, msg := range recentMessages {
totalWords += msg.WordCount()
}
avgWords := totalWords / len(recentMessages)
fmt.Printf("- Average words per message: %d\n", avgWords)
latest := recentMessages[0]
fmt.Printf("- Last message: %s (%s ago)\n",
latest.Preview(40), latest.Age())
}
// Check for mention patterns
allUserMessages, _ := babble.ByAuthor(db, username)
mentionCount := 0
for _, msg := range allUserMessages {
mentionCount += len(msg.GetMentions())
}
if len(allUserMessages) > 0 {
avgMentions := float64(mentionCount) / float64(len(allUserMessages))
fmt.Printf("- Average mentions per message: %.2f\n", avgMentions)
}
}
# Performance Considerations
## Efficient Queries
All time-based queries are optimized:
// Recent messages are efficiently ordered
recent, _ := babble.Recent(db, 100) // Uses LIMIT for efficiency
// Time-based queries use indexed timestamp
since, _ := babble.Since(db, timestamp) // Efficient with proper index
// Author queries support case-insensitive search
authorMessages, _ := babble.ByAuthor(db, "username") // Uses LOWER() function
## Memory Management
For large chat histories, process in batches:
// Process messages in batches
func processAllMessages(db *database.DB, batchSize int) {
allMessages, _ := babble.All(db)
for i := 0; i < len(allMessages); i += batchSize {
end := i + batchSize
if end > len(allMessages) {
end = len(allMessages)
}
batch := allMessages[i:end]
processBatch(batch)
}
}
# Integration Examples
## Real-Time Chat
Implement live chat updates:
func pollForNewMessages(db *database.DB, lastMessageID int) []*babble.Babble {
// Get messages newer than last seen
allMessages, _ := babble.All(db)
newMessages := make([]*babble.Babble, 0)
for _, msg := range allMessages {
if msg.ID > lastMessageID {
newMessages = append(newMessages, msg)
}
}
return newMessages
}
## Chat Commands
Process special chat commands:
func processMessage(db *database.DB, author, content string) {
// Check for commands
if strings.HasPrefix(content, "/") {
handleCommand(author, content)
return
}
// Regular chat message
msg, err := babble.NewBuilder(db).
WithAuthor(author).
WithBabble(content).
Create()
if err != nil {
log.Printf("Failed to save message: %v", err)
return
}
// Process mentions for notifications
mentions := msg.GetMentions()
for _, username := range mentions {
sendMentionNotification(username, msg)
}
}
# Error Handling
All functions return appropriate errors for common failure cases:
- Message not found (Find returns error for non-existent IDs)
- Database connection issues
- Invalid operations (e.g., saving/deleting messages without IDs)
- Search query errors
- Time range validation errors
*/
package babble