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{}
|
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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user