A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add bluesky post with status

+302 -3
+44 -3
cmd/hold/main.go
··· 5 5 "fmt" 6 6 "log" 7 7 "net/http" 8 + "os" 9 + "os/signal" 10 + "syscall" 11 + "time" 8 12 9 13 "atcr.io/pkg/hold" 10 14 "atcr.io/pkg/hold/oci" ··· 122 126 WriteTimeout: cfg.Server.WriteTimeout, 123 127 } 124 128 125 - // Start server in goroutine so we can do auto-registration after it's running 129 + // Set up signal handling for graceful shutdown 130 + sigChan := make(chan os.Signal, 1) 131 + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) 132 + 133 + // Start server in goroutine 126 134 serverErr := make(chan error, 1) 127 135 go func() { 128 136 log.Printf("Starting hold service on %s", cfg.Server.Addr) ··· 131 139 } 132 140 }() 133 141 134 - // Wait for server error or shutdown 135 - if err := <-serverErr; err != nil { 142 + // Update status post to "online" after server starts 143 + if holdPDS != nil { 144 + ctx := context.Background() 145 + if err := holdPDS.SetStatus(ctx, "online"); err != nil { 146 + log.Printf("Warning: Failed to set status post to online: %v", err) 147 + } else { 148 + log.Printf("Status post set to online") 149 + } 150 + } 151 + 152 + // Wait for signal or server error 153 + select { 154 + case err := <-serverErr: 136 155 log.Fatalf("Server failed: %v", err) 156 + case sig := <-sigChan: 157 + log.Printf("Received signal %v, shutting down gracefully...", sig) 158 + 159 + // Update status post to "offline" before shutdown 160 + if holdPDS != nil { 161 + ctx := context.Background() 162 + if err := holdPDS.SetStatus(ctx, "offline"); err != nil { 163 + log.Printf("Warning: Failed to set status post to offline: %v", err) 164 + } else { 165 + log.Printf("Status post set to offline") 166 + } 167 + } 168 + 169 + // Graceful shutdown with 10 second timeout 170 + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 171 + defer cancel() 172 + 173 + if err := server.Shutdown(shutdownCtx); err != nil { 174 + log.Printf("Server shutdown error: %v", err) 175 + } else { 176 + log.Printf("Server shutdown complete") 177 + } 137 178 } 138 179 }
+96
pkg/hold/pds/status.go
··· 1 + package pds 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "time" 7 + 8 + bsky "github.com/bluesky-social/indigo/api/bsky" 9 + "github.com/ipfs/go-cid" 10 + ) 11 + 12 + const ( 13 + // StatusPostRkey is the fixed rkey for the status post (singleton) 14 + StatusPostRkey = "status" 15 + 16 + // StatusPostCollection is the collection name for Bluesky posts 17 + StatusPostCollection = "app.bsky.feed.post" 18 + ) 19 + 20 + // SetStatus creates or updates the hold's status post on Bluesky 21 + // status should be "online" or "offline" 22 + func (p *HoldPDS) SetStatus(ctx context.Context, status string) error { 23 + // Format the post text with emoji indicator 24 + emoji := "🟢" 25 + if status == "offline" { 26 + emoji = "🔴" 27 + } 28 + text := fmt.Sprintf("%s Current status: %s", emoji, status) 29 + 30 + // Check if status post already exists 31 + _, existingPost, err := p.GetStatusPost(ctx) 32 + if err != nil { 33 + // Post doesn't exist, create it 34 + return p.createStatusPost(ctx, text) 35 + } 36 + 37 + // Post exists, update it 38 + // We need to preserve the original CreatedAt timestamp 39 + return p.updateStatusPost(ctx, text, existingPost.CreatedAt) 40 + } 41 + 42 + // GetStatusPost retrieves the status post if it exists 43 + func (p *HoldPDS) GetStatusPost(ctx context.Context) (cid.Cid, *bsky.FeedPost, error) { 44 + // Use repomgr.GetRecord 45 + recordCID, val, err := p.repomgr.GetRecord(ctx, p.uid, StatusPostCollection, StatusPostRkey, cid.Undef) 46 + if err != nil { 47 + return cid.Undef, nil, fmt.Errorf("failed to get status post: %w", err) 48 + } 49 + 50 + // Type assert to bsky.FeedPost 51 + post, ok := val.(*bsky.FeedPost) 52 + if !ok { 53 + return cid.Undef, nil, fmt.Errorf("unexpected type for status post: %T", val) 54 + } 55 + 56 + return recordCID, post, nil 57 + } 58 + 59 + // createStatusPost creates a new status post (first time) 60 + func (p *HoldPDS) createStatusPost(ctx context.Context, text string) error { 61 + // Create post struct 62 + now := time.Now().Format(time.RFC3339) 63 + post := &bsky.FeedPost{ 64 + LexiconTypeID: "app.bsky.feed.post", 65 + Text: text, 66 + CreatedAt: now, 67 + } 68 + 69 + // Use repomgr.PutRecord - creates with explicit rkey, fails if already exists 70 + recordPath, recordCID, err := p.repomgr.PutRecord(ctx, p.uid, StatusPostCollection, StatusPostRkey, post) 71 + if err != nil { 72 + return fmt.Errorf("failed to create status post: %w", err) 73 + } 74 + 75 + fmt.Printf("Created status post at %s, cid: %s, text: %s\n", recordPath, recordCID, text) 76 + return nil 77 + } 78 + 79 + // updateStatusPost updates an existing status post 80 + func (p *HoldPDS) updateStatusPost(ctx context.Context, text string, createdAt string) error { 81 + // Create updated post struct with original CreatedAt 82 + post := &bsky.FeedPost{ 83 + LexiconTypeID: "app.bsky.feed.post", 84 + Text: text, 85 + CreatedAt: createdAt, // Preserve original creation time 86 + } 87 + 88 + // Use repomgr.UpdateRecord 89 + recordCID, err := p.repomgr.UpdateRecord(ctx, p.uid, StatusPostCollection, StatusPostRkey, post) 90 + if err != nil { 91 + return fmt.Errorf("failed to update status post: %w", err) 92 + } 93 + 94 + fmt.Printf("Updated status post, cid: %s, text: %s\n", recordCID, text) 95 + return nil 96 + }
+162
pkg/hold/pds/status_test.go
··· 1 + package pds 2 + 3 + import ( 4 + "context" 5 + "os" 6 + "path/filepath" 7 + "testing" 8 + 9 + bsky "github.com/bluesky-social/indigo/api/bsky" 10 + ) 11 + 12 + func TestStatusPost(t *testing.T) { 13 + // Create temporary directory for test database 14 + tmpDir := t.TempDir() 15 + dbPath := filepath.Join(tmpDir, "test.db") 16 + keyPath := filepath.Join(tmpDir, "test.key") 17 + 18 + // Create test PDS 19 + ctx := context.Background() 20 + did := "did:web:test.example.com" 21 + publicURL := "https://test.example.com" 22 + 23 + holdPDS, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath) 24 + if err != nil { 25 + t.Fatalf("Failed to create test PDS: %v", err) 26 + } 27 + 28 + // Initialize empty repo (required before creating records) 29 + err = holdPDS.repomgr.InitNewActor(ctx, holdPDS.uid, "", did, "", "", "") 30 + if err != nil { 31 + t.Fatalf("Failed to initialize repo: %v", err) 32 + } 33 + 34 + t.Run("CreateStatusPost", func(t *testing.T) { 35 + // Set status to online (creates new post) 36 + err := holdPDS.SetStatus(ctx, "online") 37 + if err != nil { 38 + t.Fatalf("Failed to set status to online: %v", err) 39 + } 40 + 41 + // Verify post was created 42 + _, post, err := holdPDS.GetStatusPost(ctx) 43 + if err != nil { 44 + t.Fatalf("Failed to get status post: %v", err) 45 + } 46 + 47 + if post.Text != "🟢 Current status: online" { 48 + t.Errorf("Expected text '🟢 Current status: online', got '%s'", post.Text) 49 + } 50 + 51 + if post.LexiconTypeID != "app.bsky.feed.post" { 52 + t.Errorf("Expected LexiconTypeID 'app.bsky.feed.post', got '%s'", post.LexiconTypeID) 53 + } 54 + 55 + if post.CreatedAt == "" { 56 + t.Error("CreatedAt should not be empty") 57 + } 58 + }) 59 + 60 + t.Run("UpdateStatusPost", func(t *testing.T) { 61 + // Get the original post to check CreatedAt preservation 62 + _, originalPost, err := holdPDS.GetStatusPost(ctx) 63 + if err != nil { 64 + t.Fatalf("Failed to get original status post: %v", err) 65 + } 66 + 67 + // Set status to offline (updates existing post) 68 + err = holdPDS.SetStatus(ctx, "offline") 69 + if err != nil { 70 + t.Fatalf("Failed to set status to offline: %v", err) 71 + } 72 + 73 + // Verify post was updated 74 + _, post, err := holdPDS.GetStatusPost(ctx) 75 + if err != nil { 76 + t.Fatalf("Failed to get updated status post: %v", err) 77 + } 78 + 79 + if post.Text != "🔴 Current status: offline" { 80 + t.Errorf("Expected text '🔴 Current status: offline', got '%s'", post.Text) 81 + } 82 + 83 + // Verify CreatedAt was preserved 84 + if post.CreatedAt != originalPost.CreatedAt { 85 + t.Errorf("CreatedAt should be preserved. Expected '%s', got '%s'", originalPost.CreatedAt, post.CreatedAt) 86 + } 87 + }) 88 + 89 + t.Run("ToggleStatus", func(t *testing.T) { 90 + // Toggle back to online 91 + err := holdPDS.SetStatus(ctx, "online") 92 + if err != nil { 93 + t.Fatalf("Failed to set status to online: %v", err) 94 + } 95 + 96 + _, post, err := holdPDS.GetStatusPost(ctx) 97 + if err != nil { 98 + t.Fatalf("Failed to get status post: %v", err) 99 + } 100 + 101 + if post.Text != "🟢 Current status: online" { 102 + t.Errorf("Expected text '🟢 Current status: online', got '%s'", post.Text) 103 + } 104 + }) 105 + } 106 + 107 + func TestStatusPostCollection(t *testing.T) { 108 + // Verify constants 109 + if StatusPostCollection != "app.bsky.feed.post" { 110 + t.Errorf("Expected StatusPostCollection 'app.bsky.feed.post', got '%s'", StatusPostCollection) 111 + } 112 + 113 + if StatusPostRkey != "status" { 114 + t.Errorf("Expected StatusPostRkey 'status', got '%s'", StatusPostRkey) 115 + } 116 + } 117 + 118 + func TestGetStatusPostNotExists(t *testing.T) { 119 + // Create temporary directory for test database 120 + tmpDir := t.TempDir() 121 + dbPath := filepath.Join(tmpDir, "test.db") 122 + keyPath := filepath.Join(tmpDir, "test.key") 123 + 124 + // Create test PDS 125 + ctx := context.Background() 126 + did := "did:web:test2.example.com" 127 + publicURL := "https://test2.example.com" 128 + 129 + holdPDS, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath) 130 + if err != nil { 131 + t.Fatalf("Failed to create test PDS: %v", err) 132 + } 133 + 134 + // Initialize empty repo 135 + err = holdPDS.repomgr.InitNewActor(ctx, holdPDS.uid, "", did, "", "", "") 136 + if err != nil { 137 + t.Fatalf("Failed to initialize repo: %v", err) 138 + } 139 + 140 + // Try to get status post that doesn't exist 141 + _, _, err = holdPDS.GetStatusPost(ctx) 142 + if err == nil { 143 + t.Error("Expected error when getting non-existent status post, got nil") 144 + } 145 + } 146 + 147 + func init() { 148 + // Register FeedPost type for testing 149 + // This is normally done by the bsky package init(), but we need to ensure it's done 150 + // The actual registration happens in the bsky package, so this is a no-op 151 + // but kept for clarity 152 + _ = &bsky.FeedPost{} 153 + } 154 + 155 + // Cleanup function to remove test files 156 + func TestMain(m *testing.M) { 157 + // Run tests 158 + code := m.Run() 159 + 160 + // Cleanup is automatic with t.TempDir() 161 + os.Exit(code) 162 + }