bring back yahoo pipes!
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}