bring back yahoo pipes!
2
fork

Configure Feed

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

at main 172 lines 4.4 kB view raw
1package sources 2 3import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/mmcdole/gofeed" 9 10 "github.com/kierank/pipes/nodes" 11) 12 13type RSSSourceNode struct{} 14 15func (n *RSSSourceNode) Type() string { return "rss-source" } 16func (n *RSSSourceNode) Label() string { return "RSS Feed" } 17func (n *RSSSourceNode) Description() string { return "Fetch items from an RSS or Atom feed" } 18func (n *RSSSourceNode) Category() string { return "source" } 19func (n *RSSSourceNode) Inputs() int { return 0 } 20func (n *RSSSourceNode) Outputs() int { return 1 } 21 22func (n *RSSSourceNode) Execute(ctx context.Context, config map[string]interface{}, inputs [][]interface{}, execCtx *nodes.Context) ([]interface{}, error) { 23 url, ok := config["url"].(string) 24 if !ok || url == "" { 25 return nil, fmt.Errorf("url is required") 26 } 27 28 execCtx.Log("rss-source", "info", fmt.Sprintf("Fetching %s", url)) 29 30 // Parse feed 31 fp := gofeed.NewParser() 32 feed, err := fp.ParseURLWithContext(url, ctx) 33 if err != nil { 34 return nil, fmt.Errorf("parse feed: %w", err) 35 } 36 37 // Convert feed items to generic interface{} slices 38 var items []interface{} 39 for _, item := range feed.Items { 40 // Flatten author field - extract name if it's a Person struct 41 var author string 42 if item.Author != nil { 43 author = item.Author.Name 44 } 45 46 // Parse dates to Unix timestamps for proper sorting 47 var publishedAt int64 48 var updatedAt int64 49 if item.PublishedParsed != nil { 50 publishedAt = item.PublishedParsed.Unix() 51 } else if item.Published != "" { 52 if t, err := parseDate(item.Published); err == nil { 53 publishedAt = t.Unix() 54 } 55 } 56 if item.UpdatedParsed != nil { 57 updatedAt = item.UpdatedParsed.Unix() 58 } else if item.Updated != "" { 59 if t, err := parseDate(item.Updated); err == nil { 60 updatedAt = t.Unix() 61 } 62 } 63 64 // Extract content - prefer Content over Description 65 content := item.Description 66 if item.Content != "" { 67 content = item.Content 68 } 69 70 // Build enclosures array (for media like images, audio, video) 71 var enclosures []map[string]interface{} 72 if len(item.Enclosures) > 0 { 73 for _, enc := range item.Enclosures { 74 enclosures = append(enclosures, map[string]interface{}{ 75 "url": enc.URL, 76 "type": enc.Type, 77 "length": enc.Length, 78 }) 79 } 80 } 81 82 // Extract image URL if available 83 var imageURL string 84 if item.Image != nil { 85 imageURL = item.Image.URL 86 } 87 88 items = append(items, map[string]interface{}{ 89 "title": item.Title, 90 "description": item.Description, 91 "content": content, 92 "link": item.Link, 93 "author": author, 94 "published": item.Published, 95 "published_at": publishedAt, 96 "updated": item.Updated, 97 "updated_at": updatedAt, 98 "guid": item.GUID, 99 "categories": item.Categories, 100 "enclosures": enclosures, 101 "image": imageURL, 102 }) 103 } 104 105 // Apply limit if specified 106 if limit, ok := config["limit"].(float64); ok && limit > 0 { 107 if int(limit) < len(items) { 108 items = items[:int(limit)] 109 } 110 } 111 112 execCtx.Log("rss-source", "info", fmt.Sprintf("Retrieved %d items", len(items))) 113 114 return items, nil 115} 116 117func (n *RSSSourceNode) ValidateConfig(config map[string]interface{}) error { 118 url, ok := config["url"].(string) 119 if !ok || url == "" { 120 return fmt.Errorf("url is required") 121 } 122 123 return nil 124} 125 126func (n *RSSSourceNode) GetConfigSchema() *nodes.ConfigSchema { 127 return &nodes.ConfigSchema{ 128 Fields: []nodes.ConfigField{ 129 { 130 Name: "url", 131 Label: "Feed URL", 132 Type: "url", 133 Required: true, 134 Placeholder: "https://example.com/feed.xml", 135 HelpText: "URL of the RSS or Atom feed", 136 }, 137 { 138 Name: "limit", 139 Label: "Item Limit", 140 Type: "number", 141 Required: false, 142 DefaultValue: 50, 143 HelpText: "Maximum number of items to fetch", 144 }, 145 }, 146 } 147} 148 149// parseDate tries multiple date formats 150func parseDate(s string) (time.Time, error) { 151 formats := []string{ 152 time.RFC1123Z, 153 time.RFC1123, 154 time.RFC3339, 155 time.RFC822Z, 156 time.RFC822, 157 "Mon, 2 Jan 2006 15:04:05 MST", 158 "Mon, 2 Jan 2006 15:04:05 -0700", 159 "2006-01-02T15:04:05Z", 160 "2006-01-02T15:04:05-07:00", 161 "2006-01-02 15:04:05", 162 "2006-01-02", 163 } 164 165 for _, format := range formats { 166 if t, err := time.Parse(format, s); err == nil { 167 return t, nil 168 } 169 } 170 171 return time.Time{}, fmt.Errorf("unable to parse date: %s", s) 172}