( config: RssWebhookConfig, requestId: string, logger: Logger )
| 183 | } |
| 184 | |
| 185 | async function fetchNewRssItems( |
| 186 | config: RssWebhookConfig, |
| 187 | requestId: string, |
| 188 | logger: Logger |
| 189 | ): Promise<{ feed: RssFeed; items: RssItem[]; etag?: string; lastModified?: string }> { |
| 190 | try { |
| 191 | const urlValidation = await validateUrlWithDNS(config.feedUrl, 'feedUrl') |
| 192 | if (!urlValidation.isValid) { |
| 193 | logger.error(`[${requestId}] Invalid RSS feed URL: ${urlValidation.error}`) |
| 194 | throw new Error(`Invalid RSS feed URL: ${urlValidation.error}`) |
| 195 | } |
| 196 | |
| 197 | const headers: Record<string, string> = { |
| 198 | 'User-Agent': 'Sim/1.0 RSS Poller', |
| 199 | Accept: 'application/rss+xml, application/xml, text/xml, */*', |
| 200 | } |
| 201 | if (config.etag) { |
| 202 | headers['If-None-Match'] = config.etag |
| 203 | } |
| 204 | if (config.lastModified) { |
| 205 | headers['If-Modified-Since'] = config.lastModified |
| 206 | } |
| 207 | |
| 208 | const response = await secureFetchWithPinnedIP(config.feedUrl, urlValidation.resolvedIP!, { |
| 209 | headers, |
| 210 | timeout: 30000, |
| 211 | }) |
| 212 | |
| 213 | if (response.status === 304) { |
| 214 | logger.info(`[${requestId}] RSS feed not modified (304) for ${config.feedUrl}`) |
| 215 | return { |
| 216 | feed: { items: [] } as RssFeed, |
| 217 | items: [], |
| 218 | etag: response.headers.get('etag') ?? config.etag, |
| 219 | lastModified: response.headers.get('last-modified') ?? config.lastModified, |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | if (!response.ok) { |
| 224 | await response.text().catch(() => {}) |
| 225 | throw new Error(`Failed to fetch RSS feed: ${response.status} ${response.statusText}`) |
| 226 | } |
| 227 | |
| 228 | const newEtag = response.headers.get('etag') ?? undefined |
| 229 | const newLastModified = response.headers.get('last-modified') ?? undefined |
| 230 | |
| 231 | const xmlContent = await response.text() |
| 232 | const feed = await parser.parseString(xmlContent) |
| 233 | |
| 234 | if (!feed.items || !feed.items.length) { |
| 235 | return { feed: feed as RssFeed, items: [], etag: newEtag, lastModified: newLastModified } |
| 236 | } |
| 237 | |
| 238 | const lastCheckedTime = config.lastCheckedTimestamp |
| 239 | ? new Date(config.lastCheckedTimestamp) |
| 240 | : null |
| 241 | const lastSeenGuids = new Set(config.lastSeenGuids || []) |
| 242 |
no test coverage detected