Add RSS for profiles and relays

This commit is contained in:
Daniele Tonon
2023-11-16 22:58:40 +01:00
parent fb66025667
commit b8345f9176
8 changed files with 189 additions and 27 deletions

50
data.go
View File

@@ -5,6 +5,7 @@ import (
"fmt"
"html"
"html/template"
"regexp"
"strconv"
"strings"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/nbd-wtf/go-nostr/nip10"
"github.com/nbd-wtf/go-nostr/nip19"
sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/texttheater/golang-levenshtein/levenshtein"
)
type EnhancedEvent struct {
@@ -38,6 +40,54 @@ func (ee EnhancedEvent) Preview() template.HTML {
return template.HTML(strings.Join(processedLines, "<br/>"))
}
func (ee EnhancedEvent) RssTitle() string {
regex := regexp.MustCompile(`(?i)<br\s?/?>`)
replacedString := regex.ReplaceAllString(string(ee.Preview()), " ")
words := strings.Fields(replacedString)
title := ""
for i, word := range words {
if len(title)+len(word)+1 <= 65 { // +1 for space
if title != "" {
title += " "
}
title += word
} else {
if i > 1 { // the first word len is > 65
title = title + " ..."
} else {
title = ""
}
break
}
}
content := ee.RssContent()
distance := levenshtein.DistanceForStrings([]rune(title), []rune(content), levenshtein.DefaultOptions)
similarityThreshold := 5
if distance <= similarityThreshold {
return ""
} else {
return title
}
}
func (ee EnhancedEvent) RssContent() string {
content := basicFormatting(html.EscapeString(ee.event.Content), true, false)
// content = renderQuotesAsHTML(context.Background(), content, false)
content = linkQuotes(content)
return content
}
func (ee EnhancedEvent) Thumb() string {
imgRegex := regexp.MustCompile(`(https?://[^\s]+\.(?:png|jpe?g|gif|bmp|svg)(?:/[^\s]*)?)`)
matches := imgRegex.FindAllStringSubmatch(ee.event.Content, -1)
if len(matches) > 0 {
// The first match group captures the image URL
return matches[0][1]
}
return ""
}
func (ee EnhancedEvent) Npub() string {
npub, _ := nip19.EncodePublicKey(ee.event.PubKey)
return npub

1
go.mod
View File

@@ -56,6 +56,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/texttheater/golang-levenshtein v1.0.1 // indirect
github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect

2
go.sum
View File

@@ -189,6 +189,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=

View File

@@ -398,6 +398,7 @@ type SitemapPage struct {
// for the relay sitemap
RelayHostname string
Info *nip11.RelayInformationDocument
// for the profile and relay sitemaps
LastNotes []EnhancedEvent
@@ -409,6 +410,35 @@ type SitemapPage struct {
func (*SitemapPage) TemplateText() string { return tmplSitemap }
var (
//go:embed templates/rss.xml
tmplRSS string
RSSTemplate = tmpl.MustCompile(&RSSPage{})
)
type RSSPage struct {
Host string
ModifiedAt string
Title string
// for the profile RSS
Npub string
Metadata *sdk.ProfileMetadata
// for the relay RSS
RelayHostname string
Info *nip11.RelayInformationDocument
// for the profile and relay RSSs
LastNotes []EnhancedEvent
// for the archive RSS
PathPrefix string
Data []string
}
func (*RSSPage) TemplateText() string { return tmplRSS }
var (
//go:embed templates/error.html
tmplError string

View File

@@ -10,12 +10,17 @@ import (
func renderProfile(w http.ResponseWriter, r *http.Request, code string) {
fmt.Println(r.URL.Path, "@.", r.Header.Get("user-agent"))
w.Header().Set("Content-Type", "text/html")
isSitemap := false
if strings.HasSuffix(code, ".xml") {
isSitemap = true
code = code[:len(code)-4]
isSitemap = true
}
isRSS := false
if strings.HasSuffix(code, ".rss") {
code = code[:len(code)-4]
isRSS = true
}
data, err := grabData(r.Context(), code, isSitemap)
@@ -33,7 +38,27 @@ func renderProfile(w http.ResponseWriter, r *http.Request, code string) {
errorPage.TemplateText()
w.WriteHeader(http.StatusNotFound)
ErrorTemplate.Render(w, errorPage)
} else if !isSitemap {
} else if isSitemap {
w.Header().Add("content-type", "text/xml")
w.Write([]byte(XML_HEADER))
SitemapTemplate.Render(w, &SitemapPage{
Host: s.Domain,
ModifiedAt: data.modifiedAt,
Npub: data.npub,
LastNotes: data.renderableLastNotes,
})
} else if isRSS {
w.Header().Add("content-type", "text/xml")
w.Write([]byte(XML_HEADER))
RSSTemplate.Render(w, &RSSPage{
Host: s.Domain,
ModifiedAt: data.modifiedAt,
Npub: data.npub,
Metadata: data.metadata,
LastNotes: data.renderableLastNotes,
})
} else {
w.Header().Add("content-type", "text/xml")
err = ProfileTemplate.Render(w, &ProfilePage{
HeadCommonPartial: HeadCommonPartial{IsProfile: true, TailwindDebugStuff: tailwindDebugStuff},
DetailsPartial: DetailsPartial{
@@ -56,15 +81,6 @@ func renderProfile(w http.ResponseWriter, r *http.Request, code string) {
AuthorRelays: data.authorRelays,
LastNotes: data.renderableLastNotes,
})
} else {
w.Header().Add("content-type", "text/xml")
w.Write([]byte(XML_HEADER))
SitemapTemplate.Render(w, &SitemapPage{
Host: s.Domain,
ModifiedAt: data.modifiedAt,
Npub: data.npub,
LastNotes: data.renderableLastNotes,
})
}
if err != nil {

View File

@@ -1,6 +1,7 @@
package main
import (
"fmt"
"net/http"
"strings"
"time"
@@ -9,6 +10,8 @@ import (
)
func renderRelayPage(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.URL.Path, "@.", r.Header.Get("user-agent"))
hostname := r.URL.Path[3:]
if strings.HasPrefix(hostname, "wss:/") || strings.HasPrefix(hostname, "ws:/") {
@@ -18,12 +21,17 @@ func renderRelayPage(w http.ResponseWriter, r *http.Request) {
}
isSitemap := false
if strings.HasSuffix(hostname, ".xml") {
hostname = hostname[:len(hostname)-4]
isSitemap = true
}
isRSS := false
if strings.HasSuffix(hostname, ".rss") {
hostname = hostname[:len(hostname)-4]
isRSS = true
}
// relay metadata
info, err := nip11.Fetch(r.Context(), hostname)
if err != nil {
@@ -60,20 +68,7 @@ func renderRelayPage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=60")
}
if !isSitemap {
RelayTemplate.Render(w, &RelayPage{
HeadCommonPartial: HeadCommonPartial{IsProfile: false, TailwindDebugStuff: tailwindDebugStuff},
ClientsPartial: ClientsPartial{
Clients: generateRelayBrowserClientList(hostname),
},
Info: info,
Hostname: hostname,
Proxy: "https://" + hostname + "/njump/proxy?src=",
LastNotes: renderableLastNotes,
ModifiedAt: lastEventAt.Format("2006-01-02T15:04:05Z07:00"),
})
} else {
if isSitemap {
w.Header().Add("content-type", "text/xml")
w.Write([]byte(XML_HEADER))
SitemapTemplate.Render(w, &SitemapPage{
@@ -81,6 +76,31 @@ func renderRelayPage(w http.ResponseWriter, r *http.Request) {
ModifiedAt: lastEventAt.Format("2006-01-02T15:04:05Z07:00"),
LastNotes: renderableLastNotes,
RelayHostname: hostname,
Info: info,
})
} else if isRSS {
w.Header().Add("content-type", "text/xml")
w.Write([]byte(XML_HEADER))
RSSTemplate.Render(w, &RSSPage{
Host: s.Domain,
ModifiedAt: lastEventAt.Format("2006-01-02T15:04:05Z07:00"),
LastNotes: renderableLastNotes,
RelayHostname: hostname,
Info: info,
})
} else {
RelayTemplate.Render(w, &RelayPage{
HeadCommonPartial: HeadCommonPartial{IsProfile: false, TailwindDebugStuff: tailwindDebugStuff},
ClientsPartial: ClientsPartial{
Clients: generateRelayBrowserClientList(hostname),
},
Info: info,
Hostname: hostname,
Proxy: "https://" + hostname + "/njump/proxy?src=",
LastNotes: renderableLastNotes,
ModifiedAt: lastEventAt.Format("2006-01-02T15:04:05Z07:00"),
})
}
}

34
templates/rss.xml Normal file
View File

@@ -0,0 +1,34 @@
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<pubDate>{{.ModifiedAt}}</pubDate>
<lastBuildDate>{{.ModifiedAt}}</lastBuildDate>
<docs>https://github.com/fiatjaf/njump</docs>
<generator>https://{{.Host}}</generator>
{{if not (eq "" .Npub)}}
<title>Nostr notes by {{.Metadata.Name}}</title>
<link>https://{{.Host}}/{{.Npub}}</link>
<image>{{.Metadata.Picture}}</image>
{{end}}
{{if not (eq "" .RelayHostname)}}
<title>Nostr notes on {{.RelayHostname}}</title>
<link>https://{{.Host}}/r/{{.RelayHostname}}</link>
<image>{{.Info.Icon}}</image>
{{end}}
{{range $i, $ee := .LastNotes}}
<item>
<guid>{{$ee.Nevent}}</guid>
{{if not (eq "" $ee.RssTitle)}}
<title>{{$ee.RssTitle}}</title>
{{end}}
<link>https://{{$.Host}}/{{$ee.Nevent}}</link>
{{if not (eq "" $ee.Thumb)}}
<enclosure url="{{$ee.Thumb}}" />
{{end}}
<description>{{$ee.RssContent}}</description>
<pubDate>{{$ee.ModifiedAtStr}}</pubDate>
</item>
{{end}}
</channel>
</rss>

View File

@@ -317,6 +317,15 @@ func renderQuotesAsHTML(ctx context.Context, input string, usingTelegramInstantV
})
}
func linkQuotes(input string) string {
return nostrNoteNeventMatcher.ReplaceAllStringFunc(input, func(match string) string {
nip19 := match[len("nostr:"):]
first_chars := nip19[:8]
last_chars := nip19[len(nip19)-4:]
return fmt.Sprintf(`<a href="/%s">%s</a>`, nip19, first_chars+"…"+last_chars)
})
}
func sanitizeXSS(html string) string {
p := bluemonday.UGCPolicy()
p.AllowStyling()