Commit: 750fd099ba80b548cf374f87dffd7cb62af4c2d2 Parent: b78abfa9018f1bc9ae0caaf2d130f950082d42a8 Author: Vi Grey Date: 2023-08-18 07:33 UTC Summary: Remove live RSS functionality CHANGELOG.md | 11 +++++++ src/config.go | 9 ------ src/gemini.go | 29 +++++++++--------- src/gopher.go | 39 ++++++++++--------------- src/http.go | 62 ++++++++++++++++++--------------------- src/init.go | 26 ----------------- src/mime.go | 6 ++++ src/rss.go | 126 ++++++++++--------------------------------------------------------------------- 8 files changed, 89 insertions(+), 219 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a30248..78a8c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +## [0.0.24] - 2023-08-18 + +### Added + +- Ability to use static .rss or .atom file + +### Removed + +- RSS generation code + + ## [0.0.23] - 2023-08-16 ### Changed diff --git a/src/config.go b/src/config.go index 06ff5f2..51077f5 100644 --- a/src/config.go +++ b/src/config.go @@ -21,7 +21,6 @@ type Config struct { Gemini ConfigGemini `yaml:"gemini"` HTTP ConfigHTTP `yaml:"http"` Gopher ConfigGopher `yaml:"gopher"` - RSS ConfigRSS `yaml:"rss"` Finger ConfigFinger `yaml:"finger"` } @@ -85,14 +84,6 @@ type ConfigGopherTor struct { ListeningLocation string `yaml:"listening_location"` } -type ConfigRSS struct { - Enabled bool `yaml:"enabled"` - FeedSourceGeminiPath string `yaml:"feed_source_gemini_path"` - MaxEntries int `yaml:"max_entries"` - Copyright string `yaml:"copyright"` - Description string `yaml:"description"` -} - type ConfigFinger struct { Enabled bool `yaml:"enabled"` ListeningLocation string `yaml:"listening_location"` diff --git a/src/gemini.go b/src/gemini.go index f74a8c5..0cd522b 100644 --- a/src/gemini.go +++ b/src/gemini.go @@ -144,27 +144,24 @@ func handleGeminiResponseBody(conn net.Conn, urlPath, host string) { urlExtension := filepath.Ext(urlPath) gmiFilePath, _ := gemtextFileExists(urlPath) geminiDataPath := safeJoinPath(configData.Gemini.DataPath, gmiFilePath) - if !isRSSFeed(urlPath) { - mimeType := "text/gemini" - content, exists := getGemtextContent(geminiDataPath) - if exists { - err := sendGeminiResponseHeader(conn, STATUS_SUCCESS, mimeType) - if err == nil { - conn.Write(content) - } + mimeType := "text/gemini" + content, exists := getGemtextContent(geminiDataPath) + if exists { + err := sendGeminiResponseHeader(conn, STATUS_SUCCESS, mimeType) + if err == nil { + conn.Write(content) + } + } else if isRSSFeed(urlPath) { + handleGeminiServeFile(conn, geminiDataPath) + } else { + rssFilePath, isRSSFile := rssFileExists(urlPath, false, true) + if isRSSFile { + handleGeminiServeFile(conn, rssFilePath) } else if urlExtension == "" { sendGeminiResponseHeader(conn, STATUS_NOT_FOUND, "Page Not Found") } else { handleGeminiServeFile(conn, geminiDataPath) } - } else { - mimeType := "application/rss+xml" - if sendGeminiResponseHeader(conn, STATUS_SUCCESS, mimeType) != nil { - return - } - - buf := createRSSFeed("gemini://" + host) - io.Copy(conn, buf) } } diff --git a/src/gopher.go b/src/gopher.go index a767c3e..b490252 100644 --- a/src/gopher.go +++ b/src/gopher.go @@ -50,6 +50,8 @@ func handleGopherConnection(conn net.Conn, tor bool) { reqPath = path.Clean(reqPath) gmiFilePath, gmiFileExists := gemtextFileExists(reqPath) geminiDataPath := safeJoinPath(configData.Gemini.DataPath, gmiFilePath) + rssFilePath, isRSSFile := rssFileExists(reqPath, false, true) + rssDataPath := safeJoinPath(configData.Gemini.DataPath, rssFilePath) gopherHostname := "localhost" gopherPort := strconv.Itoa(configData.Gopher.Port) if tor { @@ -66,31 +68,21 @@ func handleGopherConnection(conn net.Conn, tor bool) { gopherContentError := []byte("3The selected resource does not exist\t" + "\t" + gopherHostname + "\t" + gopherPort + "\r\n.\r\n") - if (!gmiFileExists && filepath.Ext(reqPath) != "") || isRSSFeed(reqPath) { + if (!gmiFileExists && filepath.Ext(reqPath) != "") || isRSSFile { + contentPath := geminiDataPath // file is not a gemtext file - if isRSSFeed(reqPath) { - // RSS feed - rssFeedURL := fmt.Sprintf("gopher://%s", gopherHostname) - if gopherPort != strconv.Itoa(GOPHER_DEFAULT_PORT) { - rssFeedURL += fmt.Sprintf(":%s", gopherPort) - } - - buf := createRSSFeed(rssFeedURL + "/1/") - io.Copy(conn, buf) - + if isRSSFile { + contentPath = rssDataPath + } + f, err := os.Open(contentPath) + if err == nil { + defer f.Close() + io.Copy(conn, f) break } else { - // is a file that is not an RSS feed or gemtext file - f, err := os.Open(geminiDataPath) - if err == nil { - defer f.Close() - io.Copy(conn, f) - break - } else { - // Error or file doesn't exist - conn.Write(gopherContentError) - break - } + // Error or file doesn't exist + conn.Write(gopherContentError) + break } } else { gmiContent, exists := getGemtextContent(geminiDataPath) @@ -289,7 +281,8 @@ func translateGemtextToGopher(gmi, gopherHostname, gopherPort, gmiDirPath string gLine.port = port gLine.displayString = g.text gLine.selector = urlData.Path - if urlData.Scheme == "" && isRSSFeed(g.path) { + _, isRSSFile := rssFileExists(g.path, false, true) + if urlData.Scheme == "" && (isRSSFeed(g.path) || isRSSFile) { gLine.itemType = "0" } gopherLines = append(gopherLines, gLine) diff --git a/src/http.go b/src/http.go index 76a455c..8f06311 100644 --- a/src/http.go +++ b/src/http.go @@ -54,46 +54,40 @@ func htmlFileExists(resourcePath string) (resourceExtensionPath string, exists b func catchAll(w http.ResponseWriter, r *http.Request) { u := r.URL.Path urlExtension := filepath.Ext(u) - urlIsRSSFeed := isRSSFeed(u) - if !urlIsRSSFeed { - htmlFilePath, htmlFExists := htmlFileExists(u) - if htmlFExists { - handleHTTPFile(w, r, htmlFilePath) - return + htmlFilePath, htmlFExists := htmlFileExists(u) + if htmlFExists { + handleHTTPFile(w, r, htmlFilePath) + return + } + // HTML file doesn't exist + gmiFilePath, _ := gemtextFileExists(u) + gemtextFilePath := safeJoinPath(configData.Gemini.DataPath, gmiFilePath) + textOnly := configData.HTTP.TextOnly && textSubdomain(r.Host) + content, pageTitle, exists := geminiToHTMLFileContent(gemtextFilePath, textOnly) + if exists { + w.Header().Set("content-type", getMIMEType(".html")) + w.WriteHeader(http.StatusOK) + layoutPath := configData.HTTP.LayoutHTMLPath + if textOnly { + layoutPath = configData.HTTP.LayoutTextOnlyHTMLPath } - // HTML file doesn't exist - gmiFilePath, _ := gemtextFileExists(u) - gemtextFilePath := safeJoinPath(configData.Gemini.DataPath, gmiFilePath) - textOnly := configData.HTTP.TextOnly && textSubdomain(r.Host) - content, pageTitle, exists := geminiToHTMLFileContent(gemtextFilePath, textOnly) - if exists { - w.Header().Set("content-type", getMIMEType(".html")) - w.WriteHeader(http.StatusOK) - layoutPath := configData.HTTP.LayoutHTMLPath - if textOnly { - layoutPath = configData.HTTP.LayoutTextOnlyHTMLPath - } - htmlLayoutContent, err := os.ReadFile(layoutPath) - handleErr(err, fmt.Sprintf("Unable to read Layout HTML file %s\n", layoutPath)) - htmlLayoutContent = bytes.ReplaceAll(htmlLayoutContent, titleToken, pageTitle) - htmlLayoutContent = bytes.ReplaceAll(htmlLayoutContent, geminiContentToken, bytes.ReplaceAll(content, []byte("$"), []byte("$$"))) - htmlLayoutContent = bytes.ReplaceAll(htmlLayoutContent, pagePathToken, []byte(u)) - w.Write(htmlLayoutContent) + htmlLayoutContent, err := os.ReadFile(layoutPath) + handleErr(err, fmt.Sprintf("Unable to read Layout HTML file %s\n", layoutPath)) + htmlLayoutContent = bytes.ReplaceAll(htmlLayoutContent, titleToken, pageTitle) + htmlLayoutContent = bytes.ReplaceAll(htmlLayoutContent, geminiContentToken, bytes.ReplaceAll(content, []byte("$"), []byte("$$"))) + htmlLayoutContent = bytes.ReplaceAll(htmlLayoutContent, pagePathToken, []byte(u)) + w.Write(htmlLayoutContent) + } else if isRSSFeed(u) { + handleHTTPFile(w, r, u) + } else { + rssFilePath, isRSSFile := rssFileExists(u, true, true) + if isRSSFile { + handleHTTPFile(w, r, rssFilePath) } else if urlExtension == "" { w.WriteHeader(http.StatusNotFound) } else { handleHTTPFile(w, r, u) } - } else { - w.Header().Set("content-type", "application/rss+xml") - w.WriteHeader(http.StatusOK) - rssHost := "http" - if r.TLS != nil { - rssHost += "s" - } - rssHost += "://" + r.Host - buf := createRSSFeed(rssHost) - io.Copy(w, buf) } } diff --git a/src/init.go b/src/init.go index 4215daf..7867375 100644 --- a/src/init.go +++ b/src/init.go @@ -170,12 +170,6 @@ func initConfigData() { VirtualPort: GOPHER_DEFAULT_PORT, }, }, - RSS: ConfigRSS{ - Enabled: false, - FeedSourceGeminiPath: "/blog", - MaxEntries: 20, - Description: "My RSS Feed", - }, Finger: ConfigFinger{ Enabled: false, ServerMessageFilePath: "finger/.servermessage", @@ -334,26 +328,6 @@ func initBergelmirProject() { configData.HTTP.Tor.VirtualPort}) } } - // Ask if RSS feed should be enabled - configData.RSS.Enabled = getUserInputYN( - "Enable RSS feed at /rss and /feed URLs?", - configData.RSS.Enabled) - if configData.RSS.Enabled { - // Ask which Gemini source path is used for the RSS feed - configData.RSS.FeedSourceGeminiPath = getUserInputText( - "Gemini source path for RSS feed", - configData.RSS.FeedSourceGeminiPath) - // Ask how many entries can be in a single RSS feed - configData.RSS.MaxEntries = getUserInputInt( - "Max number of RSS entries (0 means all entries will be shown)", - 0, 2147483647, configData.RSS.MaxEntries, []int{}) - // Ask for RSS feed copyright license - configData.RSS.Copyright = getUserInputText( - "RSS feed copyright license (optional) (example: Creative Commons 4.0 International (CC BY 4.0) license)", - "") - configData.RSS.Description = getUserInputText( - "RSS feed description", configData.RSS.Description) - } // Ask if Finger server should be enabled configData.Finger.Enabled = getUserInputYN( "Enable Finger server?", configData.Finger.Enabled) diff --git a/src/mime.go b/src/mime.go index 9ad795f..6283d12 100644 --- a/src/mime.go +++ b/src/mime.go @@ -10,6 +10,12 @@ func getMIMEType(path string) (mimeType string) { mimeType = "application/octet-stream" extension := strings.ToLower(filepath.Ext(path)) if len(extension) > 0 { + if extension == ".rss" { + return "application/rss+xml" + } + if extension == ".atom" { + return "application/atom+xml" + } if extension == ".txt" { return "text/plain;charset=UTF-8" } diff --git a/src/rss.go b/src/rss.go index d3af769..417be4f 100644 --- a/src/rss.go +++ b/src/rss.go @@ -1,14 +1,9 @@ package main import ( - "bytes" - "fmt" - "net/url" - "path" + "os" "regexp" - "sort" "strings" - "time" ) var ( @@ -24,117 +19,26 @@ type feedEntry struct { // Check if rss value in config is enabled and if u is /feed or /rss func isRSSFeed(u string) bool { - return (configData.RSS.Enabled && (u == "/feed" || u == "/rss")) + mimeType := getMIMEType(u) + return (strings.Contains(mimeType, "rss") || strings.Contains(mimeType, "atom")) } -func createRSSFeed(host string) *bytes.Buffer { - gmiFilePath, _ := gemtextFileExists(configData.RSS.FeedSourceGeminiPath) - rssGeminiDataPath := safeJoinPath(configData.Gemini.DataPath, gmiFilePath) - gmiContent, exists := getGemtextContent(rssGeminiDataPath) - if exists { - return translateGemtextToRSS(string(gmiContent), host) - } - return bytes.NewBufferString("") -} - -func translateGemtextToRSS(gmi, host string) *bytes.Buffer { - rssFeedBuffer := new(bytes.Buffer) - rssFeedURL := joinPath(host, - configData.RSS.FeedSourceGeminiPath) - rssFeedEntries := []feedEntry{} - feedTitle := "" - gmi = strings.ReplaceAll(gmi, "\r\n", "\n") - gmiLines := strings.Split(gmi, "\n") - preformattedToggle := false - feedTitleSet := false - for _, line := range gmiLines { - g := parseGemtextLine(line, preformattedToggle) - switch g.lineType { - case GEMTEXT_HEADING: - if g.level == 1 && !feedTitleSet { - feedTitle = escapeHTMLQuotes(escapeHTMLContent(g.text)) - } - case GEMTEXT_PREFORMATTED_TOGGLE: - preformattedToggle = !preformattedToggle - case GEMTEXT_LINK: - entry, valid := parseTextToFeedEntry(g.text) - if valid { - gemtextPathURL, _ := url.Parse(g.path) - if !gemtextPathURL.IsAbs() { - entry.link = joinPath(host, g.path) - } else { - entry.link = g.path - } - entry.path = g.path - rssFeedEntries = append(rssFeedEntries, entry) +func rssFileExists(resourcePath string, http, gemini bool) (resourceExtensionPath string, exists bool) { + rssExtensions := []string{".rss", ".atom", ".RSS", ".ATOM"} + // Check for resourcePath.(rss/atom) + for _, extension := range rssExtensions { + if http { + if _, err := os.Stat(safeJoinPath(configData.HTTP.DataPath, + resourcePath+extension)); err == nil { + return resourcePath + extension, true } } - } - sort.Slice(rssFeedEntries, func(i, j int) bool { - return rssFeedEntries[i].pubDate > rssFeedEntries[j].pubDate - }) - if len(rssFeedEntries) > 0 { - latestFeedPubDate, _ := time.Parse(time.RFC3339, rssFeedEntries[0].pubDate) - latestPubDate := latestFeedPubDate.Format(time.RFC1123Z) - rssChannelDescription := "My Site's RSS Feed" - if configData.RSS.Description != "" { - rssChannelDescription = configData.RSS.Description - } - rssFeedBuffer.WriteString(fmt.Sprintf( - "\n"+ - "\n"+ - "\n"+ - " %s\n"+ - " %s\n"+ - " %s\n"+ - " %s\n", - feedTitle, rssFeedURL, latestPubDate, rssChannelDescription)) - if configData.RSS.Copyright != "" { - rssFeedBuffer.WriteString(fmt.Sprintf(" %s\n", - escapeHTMLQuotes(escapeHTMLContent(configData.RSS.Copyright)))) - } - rssFeedBuffer.WriteString("\n") - maxEntries := configData.RSS.MaxEntries - for i, entry := range rssFeedEntries { - if i >= maxEntries && maxEntries > 0 { - break - } - gmiFilePath, _ := gemtextFileExists(entry.path) - rssGeminiDataPath := safeJoinPath(configData.Gemini.DataPath, gmiFilePath) - gmiContent, exists := getGemtextContent(rssGeminiDataPath) - if exists { - feedPubDate, _ := time.Parse(time.RFC3339, entry.pubDate) - pubDate := feedPubDate.Format(time.RFC1123Z) - gemtextHTMLData, _ := translateGemtextToHTML(string(gmiContent), false) - gemtextHTMLDataString := strings.ReplaceAll(string(gemtextHTMLData), "]", "]") - rssFeedBuffer.WriteString(fmt.Sprintf( - " \n"+ - " %s\n"+ - " %s\n"+ - " %s\n"+ - " %s\n"+ - " \n"+ - " \n\n", entry.title, entry.link, - entry.link, pubDate, gemtextHTMLDataString)) + if gemini { + if _, err := os.Stat(safeJoinPath(configData.Gemini.DataPath, + resourcePath+extension)); err == nil { + return resourcePath + extension, true } } - rssFeedBuffer.WriteString("\n") - } - return rssFeedBuffer -} - -func parseTextToFeedEntry(text string) (entry feedEntry, valid bool) { - geminiFeedMatch := geminiFeedRe.FindStringSubmatch(text) - if len(geminiFeedMatch) > 0 { - return feedEntry{pubDate: geminiFeedMatch[1] + "T12:00:00Z", - title: escapeHTMLQuotes(escapeHTMLContent(geminiFeedMatch[2]))}, true - } else { - return } -} - -func joinPath(base, elem string) (joinedPath string) { - u, _ := url.Parse(base) - joinedPath = fmt.Sprintf("%s://%s", u.Scheme, path.Join(u.Host+u.Path, "/", elem)) return } gemini://vigrey.com/git/bergelmir/commit/750fd099ba80b548cf374f87dffd7cb62af4c2d2.txt

-- Leo's gemini proxy

-- Connecting to vigrey.com:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/plain; charset=UTF-8

-- Response ended

-- Page fetched on Mon May 20 15:25:02 2024