add rss feed #9

This commit is contained in:
2025-01-21 16:47:11 +01:00
parent cbc5bec053
commit 2a57a840a6
4 changed files with 143 additions and 0 deletions

View File

@@ -9,12 +9,14 @@ import (
type App struct {
articles *database.ArticleRepository
articleVMs *database.ArticleViewModelRepository
rssItems *database.RSSItemRepository
}
func NewApp(db *sql.DB) *App {
return &App{
articles: &database.ArticleRepository{DB: db},
articleVMs: &database.ArticleViewModelRepository{DB: db},
rssItems: &database.RSSItemRepository{DB: db},
}
}
@@ -22,6 +24,8 @@ func (app *App) Routes() http.Handler {
mux := http.NewServeMux()
// dynamic routes
mux.Handle("GET /rss.xml", http.HandlerFunc(app.RSS))
mux.Handle("GET /", http.HandlerFunc(app.Index))
mux.Handle("GET /page/{id}", http.HandlerFunc(app.Index))
mux.Handle("POST /up/search", http.HandlerFunc(app.UpSearch))

28
src/internal/app/rss.go Normal file
View File

@@ -0,0 +1,28 @@
package app
import (
"crowsnest/internal/model"
"encoding/xml"
"net/http"
)
// List the latest articles using the base template.
func (app *App) RSS(w http.ResponseWriter, req *http.Request) {
// set response headers
w.Header().Set("Content-Type", "application/rss+xml")
w.WriteHeader(http.StatusOK)
// get articles
feed, err := app.rssItems.All(30)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// write RSS feed to response
encoder := xml.NewEncoder(w)
encoder.Indent("", " ")
if err := encoder.Encode(model.RSSFeedFromItems(feed)); err != nil {
http.Error(w, "Error generating RSS feed", http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,60 @@
package database
import (
"crowsnest/internal/model"
"database/sql"
"strconv"
"time"
)
type RSSItemRepository struct {
DB *sql.DB
}
// Gets all the article as view models objects from the database. This may throw
// an error if the connection to the database fails.
func (m *RSSItemRepository) All(limit int) ([]model.RSSItem, error) {
stmt := `
SELECT a.id, a.title, a.sourceUrl, a.publishDate, d.summary
FROM articles a JOIN documents d ON a.document_id = d.id
ORDER BY a.publishDate DESC
LIMIT $1
`
rows, err := m.DB.Query(stmt, limit)
if err != nil {
return nil, err
}
items := make([]model.RSSItem, 0, limit)
for rows.Next() {
i := model.RSSItem{}
var id int
var sourceUrl string
var date time.Time
err := rows.Scan(&id, &i.Title, &sourceUrl, &date, &i.Description)
if err != nil {
return nil, err
}
// description
if i.Description == "" {
i.Description = "N/A"
}
// format date
i.PubDate = date.Format(time.RFC1123Z)
// link
i.Link = "https://crowsnest.kohout-dev.de/article/" + strconv.Itoa(id)
// identifier
i.Guid = model.RSSGuid{IsPermaLink: "false", Value: strconv.Itoa(id)}
items = append(items, i)
}
if err = rows.Err(); err != nil {
return nil, err
}
return items, nil
}

51
src/internal/model/rss.go Normal file
View File

@@ -0,0 +1,51 @@
package model
import (
"encoding/xml"
"time"
)
// RSS represents the RSS feed structure.
type RSSFeed struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
Channel RSSChannel `xml:"channel"`
}
// Channel represents the channel element in the RSS feed.
type RSSChannel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
PubDate string `xml:"pubDate"`
Items []RSSItem `xml:"item"`
}
// Item represents an individual item in the RSS feed.
type RSSItem struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
Guid RSSGuid `xml:"guid"`
PubDate string `xml:"pubDate"`
}
type RSSGuid struct {
IsPermaLink string `xml:"isPermaLink,attr,omitempty"`
Value string `xml:",chardata"`
}
func RSSFeedFromItems(items []RSSItem) *RSSFeed {
rssChannel := RSSChannel{
Title: "crowsnest",
Link: "https://crowsnest.kohout-dev.de",
Description: "N/A",
PubDate: time.Now().Format(time.RFC1123Z),
Items: items,
}
return &RSSFeed{
Version: "2.0",
Channel: rssChannel,
}
}