mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-17 22:34:25 +01:00
Add RSS for profiles and relays
This commit is contained in:
50
data.go
50
data.go
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
30
pages.go
30
pages.go
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
34
templates/rss.xml
Normal 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>
|
||||
9
utils.go
9
utils.go
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user