mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 17:24:16 +00:00
add bluesky post with status
This commit is contained in:
+44
-3
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user