package main import ( "encoding/xml" "io" "net/http" "time" lua "github.com/yuin/gopher-lua" ) type RssFeed struct { Title string // This is the title of the rss feed Link string // This is the link to original rss feed (if available) Content string // This contains the entire rss xml LastSyncTime string // This is the last time the rss feed was synced } type RssRoot struct { XMLName xml.Name `xml:"rss"` Channel RssChannel `xml:"channel"` } type RssChannel struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Image RssImage `xml:"image"` Items []RssItem `xml:"item"` } type RssItem struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Category string `xml:"category"` Guid string `xml:"guid"` PubDate string `xml:"pubDate"` } type RssImage struct { Url string `xml:"url"` Title string `xml:"title"` Link string `xml:"link"` } const luaRSSGlobalTable = "rss" const luaRSSItemTypeName = "RssItem" const luaRSSImageTypeName = "RssImage" var luaRSSItemMethods = map[string]lua.LGFunction{ "title": getSetRssItemTitle, "link": getSetRssItemLink, "description": getSetRssItemDescription, "category": getSetRssItemCategory, "guid": getSetRssItemGuid, "pubDate": getSetRssItemPubDate, } var luaRSSImageMethods = map[string]lua.LGFunction{ "title": getSetRssImageTitle, "url": getSetRssImageUrl, "link": getSetRssImageLink, } func registerRSSItemType(L *lua.LState) { logger.Debug("Registering RssItem type") mt := L.NewTypeMetatable(luaRSSItemTypeName) L.SetGlobal(luaRSSItemTypeName, mt) L.SetField(mt, "new", L.NewFunction(newRSSItem)) L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), luaRSSItemMethods)) // Create a global table called "rss" // This table will contain all of the rss helper functions rssTable := L.NewTable() L.SetGlobal(luaRSSGlobalTable, rssTable) L.SetField(rssTable, "get", L.NewFunction(getRssFeedHttp)) L.SetField(rssTable, "merge", L.NewFunction(mergeRssItems)) L.SetField(rssTable, "limit", L.NewFunction(limitRssItems)) L.SetField(rssTable, "generate", L.NewFunction(generateRssFeed)) } func registerRssImageType(L *lua.LState) { logger.Debug("Registering RssImage type") mt := L.NewTypeMetatable(luaRSSImageTypeName) L.SetGlobal(luaRSSImageTypeName, mt) L.SetField(mt, "new", L.NewFunction(newRSSImage)) L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), luaRSSImageMethods)) } func newRSSImage(L *lua.LState) int { ud := L.NewUserData() if L.GetTop() != 3 { L.ArgError(3, "Expected 3 arguments") return 0 } // TODO: Handle the error better title := L.CheckString(1) url := L.CheckString(2) link := L.CheckString(3) item := RssImage{ Url: url, Title: title, Link: link, } ud.Value = &item L.SetMetatable(ud, L.GetTypeMetatable(luaRSSImageTypeName)) L.Push(ud) return 1 } func checkRSSImage(L *lua.LState) *RssImage { ud := L.CheckUserData(1) if v, ok := ud.Value.(*RssImage); ok { return v } L.ArgError(1, "RssImage expected") return nil } func getSetRssImageTitle(L *lua.LState) int { i := checkRSSImage(L) if L.GetTop() == 2 { i.Title = L.CheckString(2) return 0 } L.Push(lua.LString(i.Title)) return 1 } func getSetRssImageUrl(L *lua.LState) int { i := checkRSSImage(L) if L.GetTop() == 2 { i.Url = L.CheckString(2) return 0 } L.Push(lua.LString(i.Url)) return 1 } func getSetRssImageLink(L *lua.LState) int { i := checkRSSImage(L) if L.GetTop() == 2 { i.Link = L.CheckString(2) return 0 } L.Push(lua.LString(i.Link)) return 1 } func checkRSSItem(L *lua.LState) *RssItem { ud := L.CheckUserData(1) if v, ok := ud.Value.(*RssItem); ok { return v } L.ArgError(1, "RssItem expected") return nil } func newRSSItem(L *lua.LState) int { ud := L.NewUserData() if L.GetTop() != 1 { L.ArgError(1, "Expected 1 argument") return 0 } item := L.CheckUserData(1) ud.Value = item L.SetMetatable(ud, L.GetTypeMetatable(luaRSSItemTypeName)) L.Push(ud) return 1 } // Getters and setters for the RssItem type func getSetRssItemTitle(L *lua.LState) int { i := checkRSSItem(L) if L.GetTop() == 2 { i.Title = L.CheckString(2) return 0 } L.Push(lua.LString(i.Title)) return 1 } func getSetRssItemLink(L *lua.LState) int { i := checkRSSItem(L) if L.GetTop() == 2 { i.Link = L.CheckString(2) return 0 } L.Push(lua.LString(i.Link)) return 1 } func getSetRssItemDescription(L *lua.LState) int { i := checkRSSItem(L) if L.GetTop() == 2 { i.Description = L.CheckString(2) return 0 } L.Push(lua.LString(i.Description)) return 1 } func getSetRssItemCategory(L *lua.LState) int { i := checkRSSItem(L) if L.GetTop() == 2 { i.Category = L.CheckString(2) return 0 } L.Push(lua.LString(i.Category)) return 1 } func getSetRssItemGuid(L *lua.LState) int { i := checkRSSItem(L) if L.GetTop() == 2 { i.Guid = L.CheckString(2) return 0 } L.Push(lua.LString(i.Guid)) return 1 } func getSetRssItemPubDate(L *lua.LState) int { i := checkRSSItem(L) if L.GetTop() == 2 { i.PubDate = L.CheckString(2) return 0 } L.Push(lua.LString(i.PubDate)) return 1 } func ParseRssFeed(body string) *RssRoot { var rss RssRoot err := xml.Unmarshal([]byte(body), &rss) if err != nil { logger.Fatal(err) return nil } return &rss } func GetHTTPRequest(url string) string { logger.Debug("GET: ", url) resp, err := http.Get(url) if err != nil { logger.Error(err) return "" } body, err := io.ReadAll(resp.Body) if err != nil { logger.Fatal(err) return "" } return string(body) } func getRssFeedHttp(L *lua.LState) int { if L.GetTop() != 2 { L.ArgError(1, "Expected 1 argument") return 0 } // Get the url from the first argument url := L.CheckString(2) // Check if the rss feed exists in the database var ret string currentTime := time.Now().UTC() if !appConfig.ShouldCache { // If the cache option is disabled, then always fetch the latest entries from the website logger.Debug("Cache option is disabled. Syncing: ", url) body := GetHTTPRequest(url) L.Push(lua.LString(body)) return 1 } // TODO: There might be some cleanup opportunities here if !DoesRssFeedExist(url) { // If it does not exist in the database, then get the latest entries from the website and insert them into the database logger.Debug("Rss feed does not exist in database. Syncing for the first time: ", url) body := GetHTTPRequest(url) feed := ParseRssFeed(body) rssFeed := RssFeed{ Title: feed.Channel.Title, Link: url, Content: string(body), LastSyncTime: currentTime.Format(time.RFC3339), } insert_rss_feed(rssFeed) ret = body } else { logger.Debug("Rss feed exists in database but sync is required: ", url) body := GetHTTPRequest(url) feed := ParseRssFeed(body) rssFeed := RssFeed{ Title: feed.Channel.Title, Link: url, Content: string(body), LastSyncTime: currentTime.Format(time.RFC3339), } update_rss_feed(rssFeed) ret = body } //TODO: If the cache option is disabled, then always fetch the latest entries from the website L.Push(lua.LString(ret)) return 1 } func mergeRssItems(L *lua.LState) int { table1 := L.CheckTable(1) table2 := L.CheckTable(2) if table1.Len() == 0 { L.Push(table2) return 1 } else if table2.Len() == 0 { L.Push(table1) return 1 } table2.ForEach(func(_ lua.LValue, value lua.LValue) { table1.Append(value) }) L.Push(table1) return 1 } func limitRssItems(L *lua.LState) int { table := L.CheckTable(1) limit := L.CheckInt(2) for i := table.Len(); i > limit; i-- { table.Remove(i) } L.Push(table) return 1 } func generateRssFeed(L *lua.LState) int { // Creating a new rss feed from scratch title := L.CheckString(1) entries := L.CheckTable(2) // For right now this will only be a string array of the description feed := RssRoot{} feed.Channel = RssChannel{ Title: title, Link: "http://example.com", Description: "This is an example rss feed", Image: RssImage{ Url: "http://example.com/image.jpg", Title: "Example Image", Link: "http://example.com", }, } // Append the items to the feed // Notably the tag as s items := []RssItem{} for i := 1; i <= entries.Len(); i++ { entry := entries.RawGetInt(i) item := RssItem{ Title: "Novemeber 2023", Link: "http://example.com", Description: entry.String(), Category: "Reverse Engineering", Guid: "http://example.com", PubDate: time.Now().UTC().Format(time.RFC3339), } items = append(items, item) } feed.Channel.Items = items output, err := xml.MarshalIndent(feed, "", " ") if err != nil { logger.Fatal(err) return 0 } L.Push(lua.LString(string(output))) return 1 }