Add item status (pass/fail) support

- Filter unpublished items by status='pass' in publish loop
- Add DeletePost function to remove posts from PDS
- Add /api/setItemStatus endpoint to mark items pass/fail
- When marking fail, deletes the Bluesky post if it was published

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
primal
2026-02-02 15:46:27 -05:00
parent e55cff3daa
commit 3b5c4ddeb2
4 changed files with 124 additions and 7 deletions
+3 -2
View File
@@ -15,7 +15,7 @@ func scanItems(rows pgx.Rows) ([]*commons.Item, error) {
item := &commons.Item{} item := &commons.Item{}
var guid, title, link, description, content, author *string var guid, title, link, description, content, author *string
var pubDate, updatedAt, publishedAt *interface{} var pubDate, updatedAt, publishedAt *interface{}
var publishedUri *string var status, publishedUri *string
var enclosureUrl, enclosureType *string var enclosureUrl, enclosureType *string
var enclosureLength *int64 var enclosureLength *int64
var imageUrlsJSON, tagsJSON *string var imageUrlsJSON, tagsJSON *string
@@ -26,7 +26,7 @@ func scanItems(rows pgx.Rows) ([]*commons.Item, error) {
&item.DiscoveredAt, &updatedAt, &item.DiscoveredAt, &updatedAt,
&enclosureUrl, &enclosureType, &enclosureLength, &enclosureUrl, &enclosureType, &enclosureLength,
&imageUrlsJSON, &tagsJSON, &imageUrlsJSON, &tagsJSON,
&publishedAt, &publishedUri, &status, &publishedAt, &publishedUri,
) )
if err != nil { if err != nil {
continue continue
@@ -38,6 +38,7 @@ func scanItems(rows pgx.Rows) ([]*commons.Item, error) {
item.Description = commons.StringValue(description) item.Description = commons.StringValue(description)
item.Content = commons.StringValue(content) item.Content = commons.StringValue(content)
item.Author = commons.StringValue(author) item.Author = commons.StringValue(author)
item.Status = commons.StringValue(status)
item.PublishedUri = commons.StringValue(publishedUri) item.PublishedUri = commons.StringValue(publishedUri)
if pubDate != nil { if pubDate != nil {
+3 -3
View File
@@ -183,14 +183,14 @@ func (s *PublisherService) publishFeedItems(feedURL, account string) {
} }
} }
// GetUnpublishedItems returns unpublished items for a feed // GetUnpublishedItems returns unpublished items for a feed (only items with status='pass')
func (s *PublisherService) GetUnpublishedItems(feedURL string, limit int) ([]*commons.Item, error) { func (s *PublisherService) GetUnpublishedItems(feedURL string, limit int) ([]*commons.Item, error) {
rows, err := s.db.Query(` rows, err := s.db.Query(`
SELECT feed_url, guid, title, link, description, content, author, pub_date, discovered_at, updated_at, SELECT feed_url, guid, title, link, description, content, author, pub_date, discovered_at, updated_at,
enclosure_url, enclosure_type, enclosure_length, image_urls, tags, enclosure_url, enclosure_type, enclosure_length, image_urls, tags,
published_at, published_uri status, published_at, published_uri
FROM items FROM items
WHERE feed_url = $1 AND published_at IS NULL WHERE feed_url = $1 AND published_at IS NULL AND status = 'pass'
ORDER BY pub_date ASC ORDER BY pub_date ASC
LIMIT $2 LIMIT $2
`, feedURL, limit) `, feedURL, limit)
+43
View File
@@ -370,6 +370,49 @@ func (p *Publisher) PublishItem(session *PDSSession, item *commons.Item) (string
return result.URI, nil return result.URI, nil
} }
// DeletePost deletes a post from the PDS by its AT URI
// AT URI format: at://did:plc:xxxxx/app.bsky.feed.post/rkey
func (p *Publisher) DeletePost(session *PDSSession, atURI string) error {
// Parse the AT URI to extract rkey
// Format: at://did/collection/rkey
parts := strings.Split(atURI, "/")
if len(parts) < 5 {
return fmt.Errorf("invalid AT URI format: %s", atURI)
}
rkey := parts[len(parts)-1]
payload := map[string]interface{}{
"repo": session.DID,
"collection": "app.bsky.feed.post",
"rkey": rkey,
}
body, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequest("POST", p.pdsHost+"/xrpc/com.atproto.repo.deleteRecord", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+session.AccessJwt)
resp, err := p.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete record failed: %s - %s", resp.Status, string(respBody))
}
return nil
}
func truncate(s string, maxLen int) string { func truncate(s string, maxLen int) string {
if len(s) <= maxLen { if len(s) <= maxLen {
return s return s
+75 -2
View File
@@ -36,6 +36,7 @@ func (s *PublisherService) StartServer(addr string) error {
mux.HandleFunc("/api/deriveHandle", s.handleDeriveHandle) mux.HandleFunc("/api/deriveHandle", s.handleDeriveHandle)
mux.HandleFunc("/api/resetAllPublishing", s.handleResetAllPublishing) mux.HandleFunc("/api/resetAllPublishing", s.handleResetAllPublishing)
mux.HandleFunc("/api/refreshProfiles", s.handleRefreshProfiles) mux.HandleFunc("/api/refreshProfiles", s.handleRefreshProfiles)
mux.HandleFunc("/api/setItemStatus", s.handleSetItemStatus)
fmt.Printf("Publisher API running at http://%s\n", addr) fmt.Printf("Publisher API running at http://%s\n", addr)
return http.ListenAndServe(addr, mux) return http.ListenAndServe(addr, mux)
@@ -505,6 +506,78 @@ func (s *PublisherService) handleRefreshProfiles(w http.ResponseWriter, r *http.
}) })
} }
// handleSetItemStatus sets an item's status to 'pass' or 'fail'
// If setting to 'fail' and the item was published, deletes the Bluesky post
func (s *PublisherService) handleSetItemStatus(w http.ResponseWriter, r *http.Request) {
feedURL := r.URL.Query().Get("feedUrl")
guid := r.URL.Query().Get("guid")
status := r.URL.Query().Get("status")
if feedURL == "" || guid == "" {
http.Error(w, "feedUrl and guid parameters required", http.StatusBadRequest)
return
}
if status != "pass" && status != "fail" {
http.Error(w, "status must be 'pass' or 'fail'", http.StatusBadRequest)
return
}
// Get the item to check if it was published
item, err := s.GetItemByGUID(feedURL, guid)
if err != nil {
http.Error(w, "item not found: "+err.Error(), http.StatusNotFound)
return
}
result := map[string]interface{}{
"feedUrl": feedURL,
"guid": guid,
"status": status,
}
// If setting to fail and item was published, delete the post
if status == "fail" && item.PublishedUri != "" {
// Get the feed's publish account to authenticate
var account string
err := s.db.QueryRow(`SELECT publish_account FROM feeds WHERE url = $1`, feedURL).Scan(&account)
if err != nil || account == "" {
http.Error(w, "could not find publish account for feed", http.StatusInternalServerError)
return
}
// Authenticate and delete the post
session, err := s.publisher.CreateSession(account, s.feedPassword)
if err != nil {
http.Error(w, "auth failed: "+err.Error(), http.StatusUnauthorized)
return
}
if err := s.publisher.DeletePost(session, item.PublishedUri); err != nil {
// Log but don't fail - the post might already be deleted
fmt.Printf("Warning: failed to delete post %s: %v\n", item.PublishedUri, err)
result["delete_warning"] = err.Error()
} else {
result["post_deleted"] = true
}
// Clear the published fields
_, err = s.db.Exec(`UPDATE items SET published_at = NULL, published_uri = NULL WHERE feed_url = $1 AND guid = $2`, feedURL, guid)
if err != nil {
fmt.Printf("Warning: failed to clear published fields: %v\n", err)
}
}
// Update the item status
_, err = s.db.Exec(`UPDATE items SET status = $1 WHERE feed_url = $2 AND guid = $3`, status, feedURL, guid)
if err != nil {
http.Error(w, "failed to update status: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// Database helper methods // Database helper methods
func (s *PublisherService) SetPublishStatus(feedURL, status, account string) error { func (s *PublisherService) SetPublishStatus(feedURL, status, account string) error {
@@ -567,7 +640,7 @@ func (s *PublisherService) GetPublishCandidates(limit int) ([]*commons.Feed, err
func (s *PublisherService) GetUnpublishedItemCount(feedURL string) (int, error) { func (s *PublisherService) GetUnpublishedItemCount(feedURL string) (int, error) {
var count int var count int
err := s.db.QueryRow(` err := s.db.QueryRow(`
SELECT COUNT(*) FROM items WHERE feed_url = $1 AND published_at IS NULL SELECT COUNT(*) FROM items WHERE feed_url = $1 AND published_at IS NULL AND status = 'pass'
`, feedURL).Scan(&count) `, feedURL).Scan(&count)
return count, err return count, err
} }
@@ -576,7 +649,7 @@ func (s *PublisherService) GetItemByGUID(feedURL, guid string) (*commons.Item, e
items, err := s.db.Query(` items, err := s.db.Query(`
SELECT feed_url, guid, title, link, description, content, author, pub_date, discovered_at, updated_at, SELECT feed_url, guid, title, link, description, content, author, pub_date, discovered_at, updated_at,
enclosure_url, enclosure_type, enclosure_length, image_urls, tags, enclosure_url, enclosure_type, enclosure_length, image_urls, tags,
published_at, published_uri status, published_at, published_uri
FROM items FROM items
WHERE feed_url = $1 AND guid = $2 WHERE feed_url = $1 AND guid = $2
`, feedURL, guid) `, feedURL, guid)