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:
@@ -15,7 +15,7 @@ func scanItems(rows pgx.Rows) ([]*commons.Item, error) {
|
||||
item := &commons.Item{}
|
||||
var guid, title, link, description, content, author *string
|
||||
var pubDate, updatedAt, publishedAt *interface{}
|
||||
var publishedUri *string
|
||||
var status, publishedUri *string
|
||||
var enclosureUrl, enclosureType *string
|
||||
var enclosureLength *int64
|
||||
var imageUrlsJSON, tagsJSON *string
|
||||
@@ -26,7 +26,7 @@ func scanItems(rows pgx.Rows) ([]*commons.Item, error) {
|
||||
&item.DiscoveredAt, &updatedAt,
|
||||
&enclosureUrl, &enclosureType, &enclosureLength,
|
||||
&imageUrlsJSON, &tagsJSON,
|
||||
&publishedAt, &publishedUri,
|
||||
&status, &publishedAt, &publishedUri,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -38,6 +38,7 @@ func scanItems(rows pgx.Rows) ([]*commons.Item, error) {
|
||||
item.Description = commons.StringValue(description)
|
||||
item.Content = commons.StringValue(content)
|
||||
item.Author = commons.StringValue(author)
|
||||
item.Status = commons.StringValue(status)
|
||||
item.PublishedUri = commons.StringValue(publishedUri)
|
||||
|
||||
if pubDate != nil {
|
||||
|
||||
@@ -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) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT feed_url, guid, title, link, description, content, author, pub_date, discovered_at, updated_at,
|
||||
enclosure_url, enclosure_type, enclosure_length, image_urls, tags,
|
||||
published_at, published_uri
|
||||
status, published_at, published_uri
|
||||
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
|
||||
LIMIT $2
|
||||
`, feedURL, limit)
|
||||
|
||||
@@ -370,6 +370,49 @@ func (p *Publisher) PublishItem(session *PDSSession, item *commons.Item) (string
|
||||
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 {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
|
||||
@@ -36,6 +36,7 @@ func (s *PublisherService) StartServer(addr string) error {
|
||||
mux.HandleFunc("/api/deriveHandle", s.handleDeriveHandle)
|
||||
mux.HandleFunc("/api/resetAllPublishing", s.handleResetAllPublishing)
|
||||
mux.HandleFunc("/api/refreshProfiles", s.handleRefreshProfiles)
|
||||
mux.HandleFunc("/api/setItemStatus", s.handleSetItemStatus)
|
||||
|
||||
fmt.Printf("Publisher API running at http://%s\n", addr)
|
||||
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
|
||||
|
||||
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) {
|
||||
var count int
|
||||
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)
|
||||
return count, err
|
||||
}
|
||||
@@ -576,7 +649,7 @@ func (s *PublisherService) GetItemByGUID(feedURL, guid string) (*commons.Item, e
|
||||
items, err := s.db.Query(`
|
||||
SELECT feed_url, guid, title, link, description, content, author, pub_date, discovered_at, updated_at,
|
||||
enclosure_url, enclosure_type, enclosure_length, image_urls, tags,
|
||||
published_at, published_uri
|
||||
status, published_at, published_uri
|
||||
FROM items
|
||||
WHERE feed_url = $1 AND guid = $2
|
||||
`, feedURL, guid)
|
||||
|
||||
Reference in New Issue
Block a user