add bluesky post with status

This commit is contained in:
Evan Jarrett
2025-10-22 18:38:43 -05:00
parent 1b1400a6fb
commit 3809bcab25
3 changed files with 302 additions and 3 deletions
+44 -3
View File
@@ -5,6 +5,10 @@ import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"atcr.io/pkg/hold"
"atcr.io/pkg/hold/oci"
@@ -122,7 +126,11 @@ func main() {
WriteTimeout: cfg.Server.WriteTimeout,
}
// Start server in goroutine so we can do auto-registration after it's running
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// Start server in goroutine
serverErr := make(chan error, 1)
go func() {
log.Printf("Starting hold service on %s", cfg.Server.Addr)
@@ -131,8 +139,41 @@ func main() {
}
}()
// Wait for server error or shutdown
if err := <-serverErr; err != nil {
// Update status post to "online" after server starts
if holdPDS != nil {
ctx := context.Background()
if err := holdPDS.SetStatus(ctx, "online"); err != nil {
log.Printf("Warning: Failed to set status post to online: %v", err)
} else {
log.Printf("Status post set to online")
}
}
// Wait for signal or server error
select {
case err := <-serverErr:
log.Fatalf("Server failed: %v", err)
case sig := <-sigChan:
log.Printf("Received signal %v, shutting down gracefully...", sig)
// Update status post to "offline" before shutdown
if holdPDS != nil {
ctx := context.Background()
if err := holdPDS.SetStatus(ctx, "offline"); err != nil {
log.Printf("Warning: Failed to set status post to offline: %v", err)
} else {
log.Printf("Status post set to offline")
}
}
// Graceful shutdown with 10 second timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("Server shutdown error: %v", err)
} else {
log.Printf("Server shutdown complete")
}
}
}
+96
View File
@@ -0,0 +1,96 @@
package pds
import (
"context"
"fmt"
"time"
bsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/ipfs/go-cid"
)
const (
// StatusPostRkey is the fixed rkey for the status post (singleton)
StatusPostRkey = "status"
// StatusPostCollection is the collection name for Bluesky posts
StatusPostCollection = "app.bsky.feed.post"
)
// SetStatus creates or updates the hold's status post on Bluesky
// status should be "online" or "offline"
func (p *HoldPDS) SetStatus(ctx context.Context, status string) error {
// Format the post text with emoji indicator
emoji := "🟢"
if status == "offline" {
emoji = "🔴"
}
text := fmt.Sprintf("%s Current status: %s", emoji, status)
// Check if status post already exists
_, existingPost, err := p.GetStatusPost(ctx)
if err != nil {
// Post doesn't exist, create it
return p.createStatusPost(ctx, text)
}
// Post exists, update it
// We need to preserve the original CreatedAt timestamp
return p.updateStatusPost(ctx, text, existingPost.CreatedAt)
}
// GetStatusPost retrieves the status post if it exists
func (p *HoldPDS) GetStatusPost(ctx context.Context) (cid.Cid, *bsky.FeedPost, error) {
// Use repomgr.GetRecord
recordCID, val, err := p.repomgr.GetRecord(ctx, p.uid, StatusPostCollection, StatusPostRkey, cid.Undef)
if err != nil {
return cid.Undef, nil, fmt.Errorf("failed to get status post: %w", err)
}
// Type assert to bsky.FeedPost
post, ok := val.(*bsky.FeedPost)
if !ok {
return cid.Undef, nil, fmt.Errorf("unexpected type for status post: %T", val)
}
return recordCID, post, nil
}
// createStatusPost creates a new status post (first time)
func (p *HoldPDS) createStatusPost(ctx context.Context, text string) error {
// Create post struct
now := time.Now().Format(time.RFC3339)
post := &bsky.FeedPost{
LexiconTypeID: "app.bsky.feed.post",
Text: text,
CreatedAt: now,
}
// Use repomgr.PutRecord - creates with explicit rkey, fails if already exists
recordPath, recordCID, err := p.repomgr.PutRecord(ctx, p.uid, StatusPostCollection, StatusPostRkey, post)
if err != nil {
return fmt.Errorf("failed to create status post: %w", err)
}
fmt.Printf("Created status post at %s, cid: %s, text: %s\n", recordPath, recordCID, text)
return nil
}
// updateStatusPost updates an existing status post
func (p *HoldPDS) updateStatusPost(ctx context.Context, text string, createdAt string) error {
// Create updated post struct with original CreatedAt
post := &bsky.FeedPost{
LexiconTypeID: "app.bsky.feed.post",
Text: text,
CreatedAt: createdAt, // Preserve original creation time
}
// Use repomgr.UpdateRecord
recordCID, err := p.repomgr.UpdateRecord(ctx, p.uid, StatusPostCollection, StatusPostRkey, post)
if err != nil {
return fmt.Errorf("failed to update status post: %w", err)
}
fmt.Printf("Updated status post, cid: %s, text: %s\n", recordCID, text)
return nil
}
+162
View File
@@ -0,0 +1,162 @@
package pds
import (
"context"
"os"
"path/filepath"
"testing"
bsky "github.com/bluesky-social/indigo/api/bsky"
)
func TestStatusPost(t *testing.T) {
// Create temporary directory for test database
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
keyPath := filepath.Join(tmpDir, "test.key")
// Create test PDS
ctx := context.Background()
did := "did:web:test.example.com"
publicURL := "https://test.example.com"
holdPDS, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath)
if err != nil {
t.Fatalf("Failed to create test PDS: %v", err)
}
// Initialize empty repo (required before creating records)
err = holdPDS.repomgr.InitNewActor(ctx, holdPDS.uid, "", did, "", "", "")
if err != nil {
t.Fatalf("Failed to initialize repo: %v", err)
}
t.Run("CreateStatusPost", func(t *testing.T) {
// Set status to online (creates new post)
err := holdPDS.SetStatus(ctx, "online")
if err != nil {
t.Fatalf("Failed to set status to online: %v", err)
}
// Verify post was created
_, post, err := holdPDS.GetStatusPost(ctx)
if err != nil {
t.Fatalf("Failed to get status post: %v", err)
}
if post.Text != "🟢 Current status: online" {
t.Errorf("Expected text '🟢 Current status: online', got '%s'", post.Text)
}
if post.LexiconTypeID != "app.bsky.feed.post" {
t.Errorf("Expected LexiconTypeID 'app.bsky.feed.post', got '%s'", post.LexiconTypeID)
}
if post.CreatedAt == "" {
t.Error("CreatedAt should not be empty")
}
})
t.Run("UpdateStatusPost", func(t *testing.T) {
// Get the original post to check CreatedAt preservation
_, originalPost, err := holdPDS.GetStatusPost(ctx)
if err != nil {
t.Fatalf("Failed to get original status post: %v", err)
}
// Set status to offline (updates existing post)
err = holdPDS.SetStatus(ctx, "offline")
if err != nil {
t.Fatalf("Failed to set status to offline: %v", err)
}
// Verify post was updated
_, post, err := holdPDS.GetStatusPost(ctx)
if err != nil {
t.Fatalf("Failed to get updated status post: %v", err)
}
if post.Text != "🔴 Current status: offline" {
t.Errorf("Expected text '🔴 Current status: offline', got '%s'", post.Text)
}
// Verify CreatedAt was preserved
if post.CreatedAt != originalPost.CreatedAt {
t.Errorf("CreatedAt should be preserved. Expected '%s', got '%s'", originalPost.CreatedAt, post.CreatedAt)
}
})
t.Run("ToggleStatus", func(t *testing.T) {
// Toggle back to online
err := holdPDS.SetStatus(ctx, "online")
if err != nil {
t.Fatalf("Failed to set status to online: %v", err)
}
_, post, err := holdPDS.GetStatusPost(ctx)
if err != nil {
t.Fatalf("Failed to get status post: %v", err)
}
if post.Text != "🟢 Current status: online" {
t.Errorf("Expected text '🟢 Current status: online', got '%s'", post.Text)
}
})
}
func TestStatusPostCollection(t *testing.T) {
// Verify constants
if StatusPostCollection != "app.bsky.feed.post" {
t.Errorf("Expected StatusPostCollection 'app.bsky.feed.post', got '%s'", StatusPostCollection)
}
if StatusPostRkey != "status" {
t.Errorf("Expected StatusPostRkey 'status', got '%s'", StatusPostRkey)
}
}
func TestGetStatusPostNotExists(t *testing.T) {
// Create temporary directory for test database
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "test.db")
keyPath := filepath.Join(tmpDir, "test.key")
// Create test PDS
ctx := context.Background()
did := "did:web:test2.example.com"
publicURL := "https://test2.example.com"
holdPDS, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath)
if err != nil {
t.Fatalf("Failed to create test PDS: %v", err)
}
// Initialize empty repo
err = holdPDS.repomgr.InitNewActor(ctx, holdPDS.uid, "", did, "", "", "")
if err != nil {
t.Fatalf("Failed to initialize repo: %v", err)
}
// Try to get status post that doesn't exist
_, _, err = holdPDS.GetStatusPost(ctx)
if err == nil {
t.Error("Expected error when getting non-existent status post, got nil")
}
}
func init() {
// Register FeedPost type for testing
// This is normally done by the bsky package init(), but we need to ensure it's done
// The actual registration happens in the bsky package, so this is a no-op
// but kept for clarity
_ = &bsky.FeedPost{}
}
// Cleanup function to remove test files
func TestMain(m *testing.M) {
// Run tests
code := m.Run()
// Cleanup is automatic with t.TempDir()
os.Exit(code)
}