index.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
  2. import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
  3. interface RSSItem {
  4. title: string;
  5. description: string;
  6. link: string;
  7. pubDate: string;
  8. guid?: string;
  9. content?: string;
  10. image?: string;
  11. }
  12. interface RSSFeed {
  13. title: string;
  14. description: string;
  15. items: RSSItem[];
  16. }
  17. const corsHeaders = {
  18. 'Access-Control-Allow-Origin': '*',
  19. 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  20. }
  21. serve(async (req) => {
  22. if (req.method === 'OPTIONS') {
  23. return new Response('ok', { headers: corsHeaders })
  24. }
  25. try {
  26. const supabaseClient = createClient(
  27. Deno.env.get('SUPABASE_URL') ?? '',
  28. Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
  29. )
  30. const { feedId, feedUrl } = await req.json()
  31. console.log(`Fetching RSS for feed: ${feedId}, URL: ${feedUrl}`)
  32. // Fetch RSS content
  33. const response = await fetch(feedUrl)
  34. if (!response.ok) {
  35. throw new Error(`Failed to fetch RSS: ${response.statusText}`)
  36. }
  37. const rssText = await response.text()
  38. // Parse RSS using DOMParser
  39. const parser = new DOMParser()
  40. const doc = parser.parseFromString(rssText, 'application/xml')
  41. if (!doc) {
  42. throw new Error('Failed to parse RSS XML')
  43. }
  44. // Extract RSS items
  45. const items = Array.from(doc.querySelectorAll('item, entry')).map(item => {
  46. const title = item.querySelector('title')?.textContent?.trim() || ''
  47. const description = item.querySelector('description, summary')?.textContent?.trim() || ''
  48. const link = item.querySelector('link')?.textContent?.trim() ||
  49. item.querySelector('link')?.getAttribute('href') || ''
  50. const pubDate = item.querySelector('pubDate, published')?.textContent?.trim() || ''
  51. const guid = item.querySelector('guid')?.textContent?.trim() || link
  52. // Try to extract image from content or enclosure
  53. let image = ''
  54. const enclosure = item.querySelector('enclosure[type^="image"]')
  55. if (enclosure) {
  56. image = enclosure.getAttribute('url') || ''
  57. } else {
  58. // Try to find image in content
  59. const content = item.querySelector('content\\:encoded, content')?.textContent
  60. if (content) {
  61. const imgMatch = content.match(/<img[^>]+src="([^">]+)"/i)
  62. if (imgMatch) {
  63. image = imgMatch[1]
  64. }
  65. }
  66. }
  67. return {
  68. title,
  69. description,
  70. link,
  71. pubDate,
  72. guid,
  73. image,
  74. content: description
  75. }
  76. }).filter(item => item.title && item.guid)
  77. console.log(`Found ${items.length} items`)
  78. // Save articles to database
  79. const articlesToInsert = items.map(item => {
  80. // Calculate read time (rough estimate: 200 words per minute)
  81. const wordCount = (item.description || '').split(' ').length
  82. const readTime = Math.max(1, Math.ceil(wordCount / 200))
  83. return {
  84. feed_id: feedId,
  85. title: item.title,
  86. description: item.description,
  87. content: item.content || item.description,
  88. url: item.link,
  89. image_url: item.image || null,
  90. published_at: item.pubDate ? new Date(item.pubDate).toISOString() : new Date().toISOString(),
  91. guid: item.guid,
  92. read_time: readTime
  93. }
  94. })
  95. // Insert articles (on conflict do nothing to avoid duplicates)
  96. const { error: insertError } = await supabaseClient
  97. .from('articles')
  98. .upsert(articlesToInsert, {
  99. onConflict: 'feed_id,guid',
  100. ignoreDuplicates: true
  101. })
  102. if (insertError) {
  103. console.error('Error inserting articles:', insertError)
  104. throw insertError
  105. }
  106. // Update feed's last_fetched_at
  107. const { error: updateError } = await supabaseClient
  108. .from('feeds')
  109. .update({
  110. last_fetched_at: new Date().toISOString(),
  111. status: 'active'
  112. })
  113. .eq('id', feedId)
  114. if (updateError) {
  115. console.error('Error updating feed:', updateError)
  116. throw updateError
  117. }
  118. console.log(`Successfully processed ${articlesToInsert.length} articles for feed ${feedId}`)
  119. return new Response(
  120. JSON.stringify({
  121. success: true,
  122. articlesProcessed: articlesToInsert.length
  123. }),
  124. {
  125. headers: { ...corsHeaders, 'Content-Type': 'application/json' }
  126. }
  127. )
  128. } catch (error) {
  129. console.error('Error in fetch-rss function:', error)
  130. return new Response(
  131. JSON.stringify({
  132. error: error.message,
  133. success: false
  134. }),
  135. {
  136. status: 500,
  137. headers: { ...corsHeaders, 'Content-Type': 'application/json' }
  138. }
  139. )
  140. }
  141. })