mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-17 06:14:22 +01:00
422 lines
12 KiB
Go
422 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html"
|
|
"html/template"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nbd-wtf/go-nostr"
|
|
"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 {
|
|
event *nostr.Event
|
|
relays []string
|
|
}
|
|
|
|
func (ee EnhancedEvent) IsReply() bool {
|
|
return nip10.GetImmediateReply(ee.event.Tags) != nil
|
|
}
|
|
|
|
func (ee EnhancedEvent) Reply() *nostr.Tag {
|
|
return nip10.GetImmediateReply(ee.event.Tags)
|
|
}
|
|
|
|
func (ee EnhancedEvent) Preview() template.HTML {
|
|
lines := strings.Split(html.EscapeString(ee.event.Content), "\n")
|
|
var processedLines []string
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
processedLine := shortenNostrURLs(line)
|
|
processedLines = append(processedLines, processedLine)
|
|
}
|
|
|
|
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 := ee.event.Content
|
|
if ee.IsReply() {
|
|
nevent, _ := nip19.EncodeEvent(ee.Reply().Value(), ee.relays, ee.event.PubKey)
|
|
content = content + "\n\n____________________\nIn reply to nostr:" + nevent
|
|
}
|
|
content = basicFormatting(html.EscapeString(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
|
|
}
|
|
|
|
func (ee EnhancedEvent) NpubShort() string {
|
|
npub := ee.Npub()
|
|
return npub[:8] + "…" + npub[len(npub)-4:]
|
|
}
|
|
|
|
func (ee EnhancedEvent) Nevent() string {
|
|
nevent, _ := nip19.EncodeEvent(ee.event.ID, ee.relays, ee.event.PubKey)
|
|
return nevent
|
|
}
|
|
|
|
func (ee EnhancedEvent) CreatedAtStr() string {
|
|
return time.Unix(int64(ee.event.CreatedAt), 0).Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
func (ee EnhancedEvent) ModifiedAtStr() string {
|
|
return time.Unix(int64(ee.event.CreatedAt), 0).Format("2006-01-02T15:04:05Z07:00")
|
|
}
|
|
|
|
type Data struct {
|
|
templateId TemplateID
|
|
event *nostr.Event
|
|
relays []string
|
|
npub string
|
|
npubShort string
|
|
nprofile string
|
|
nevent string
|
|
neventNaked string
|
|
naddr string
|
|
naddrNaked string
|
|
createdAt string
|
|
modifiedAt string
|
|
parentLink template.HTML
|
|
metadata *sdk.ProfileMetadata
|
|
authorRelays []string
|
|
authorLong string
|
|
authorShort string
|
|
renderableLastNotes []EnhancedEvent
|
|
kindDescription string
|
|
kindNIP string
|
|
video string
|
|
videoType string
|
|
image string
|
|
content string
|
|
alt string
|
|
kind1063Metadata *Kind1063Metadata
|
|
kind30311Metadata *Kind30311Metadata
|
|
kind1311Metadata *Kind1311Metadata
|
|
}
|
|
|
|
type Kind1063Metadata struct {
|
|
Magnet string
|
|
Dim string
|
|
Size string
|
|
Summary string
|
|
Image string
|
|
URL string
|
|
AES256GCM string
|
|
M string
|
|
X string
|
|
I string
|
|
Blurhash string
|
|
Thumb string
|
|
}
|
|
|
|
type Kind30311Metadata struct {
|
|
Title string
|
|
Summary string
|
|
Image string
|
|
Status string
|
|
Host sdk.ProfileMetadata
|
|
HostNpub string
|
|
Tags []string
|
|
}
|
|
|
|
type Kind1311Metadata struct {
|
|
// ...
|
|
}
|
|
|
|
func (fm Kind1063Metadata) IsVideo() bool { return strings.Split(fm.M, "/")[0] == "video" }
|
|
func (fm Kind1063Metadata) IsImage() bool { return strings.Split(fm.M, "/")[0] == "image" }
|
|
func (fm Kind1063Metadata) DisplayImage() string {
|
|
if fm.Image != "" {
|
|
return fm.Image
|
|
} else if fm.IsImage() {
|
|
return fm.URL
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, error) {
|
|
// code can be a nevent, nprofile, npub or nip05 identifier, in which case we try to fetch the associated event
|
|
event, relays, err := getEvent(ctx, code, nil)
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("code", code).Msg("failed to fetch event for code")
|
|
return nil, fmt.Errorf("error fetching event: %w", err)
|
|
}
|
|
|
|
relaysForNip19 := make([]string, 0, 3)
|
|
c := 0
|
|
for _, relayUrl := range relays {
|
|
if shouldUseRelayForNip19(relayUrl) {
|
|
relaysForNip19 = append(relaysForNip19, relayUrl)
|
|
if c == 2 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
data := &Data{
|
|
event: event,
|
|
relays: relays,
|
|
}
|
|
|
|
data.npub, _ = nip19.EncodePublicKey(event.PubKey)
|
|
data.npubShort = data.npub[:8] + "…" + data.npub[len(data.npub)-4:]
|
|
data.authorLong = data.npub // hopefully will be replaced later
|
|
data.authorShort = data.npubShort // hopefully will be replaced later
|
|
data.nevent, _ = nip19.EncodeEvent(event.ID, relaysForNip19, event.PubKey)
|
|
data.neventNaked, _ = nip19.EncodeEvent(event.ID, nil, event.PubKey)
|
|
data.naddr = ""
|
|
data.naddrNaked = ""
|
|
data.createdAt = time.Unix(int64(event.CreatedAt), 0).Format("2006-01-02 15:04:05")
|
|
data.modifiedAt = time.Unix(int64(event.CreatedAt), 0).Format("2006-01-02T15:04:05Z07:00")
|
|
data.authorRelays = []string{}
|
|
|
|
if event.Kind >= 30000 && event.Kind < 40000 {
|
|
if d := event.Tags.GetFirst([]string{"d", ""}); d != nil {
|
|
data.naddr, _ = nip19.EncodeEntity(event.PubKey, event.Kind, d.Value(), relaysForNip19)
|
|
data.naddrNaked, _ = nip19.EncodeEntity(event.PubKey, event.Kind, d.Value(), nil)
|
|
}
|
|
}
|
|
|
|
if tag := event.Tags.GetFirst([]string{"alt", ""}); tag != nil {
|
|
data.alt = (*tag)[1]
|
|
}
|
|
|
|
switch event.Kind {
|
|
case 0:
|
|
{
|
|
rawAuthorRelays := []string{}
|
|
ctx, cancel := context.WithTimeout(ctx, time.Second*4)
|
|
rawAuthorRelays = relaysForPubkey(ctx, event.PubKey)
|
|
cancel()
|
|
for _, relay := range rawAuthorRelays {
|
|
for _, excluded := range excludedRelays {
|
|
if strings.Contains(relay, excluded) {
|
|
continue
|
|
}
|
|
}
|
|
if strings.Contains(relay, "/npub1") {
|
|
continue // skip relays with personalyzed query like filter.nostr.wine
|
|
}
|
|
data.authorRelays = append(data.authorRelays, trimProtocol(relay))
|
|
}
|
|
}
|
|
|
|
lastNotes := authorLastNotes(ctx, event.PubKey, data.authorRelays, isProfileSitemap)
|
|
data.renderableLastNotes = make([]EnhancedEvent, len(lastNotes))
|
|
for i, levt := range lastNotes {
|
|
data.renderableLastNotes[i] = EnhancedEvent{levt, []string{}}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case 1, 7, 30023, 30024:
|
|
data.templateId = Note
|
|
data.content = event.Content
|
|
if parentNevent := getParentNevent(event); parentNevent != "" {
|
|
data.parentLink = template.HTML(replaceNostrURLsWithTags(nostrNoteNeventMatcher, "nostr:"+parentNevent))
|
|
}
|
|
case 6:
|
|
data.templateId = Note
|
|
if reposted := event.Tags.GetFirst([]string{"e", ""}); reposted != nil {
|
|
originalNevent, _ := nip19.EncodeEvent((*reposted)[1], []string{}, "")
|
|
data.content = "Repost of nostr:" + originalNevent
|
|
}
|
|
case 1063:
|
|
data.templateId = FileMetadata
|
|
data.kind1063Metadata = &Kind1063Metadata{}
|
|
|
|
if tag := event.Tags.GetFirst([]string{"url", ""}); tag != nil {
|
|
data.kind1063Metadata.URL = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"m", ""}); tag != nil {
|
|
data.kind1063Metadata.M = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"aes-256-gcm", ""}); tag != nil {
|
|
data.kind1063Metadata.AES256GCM = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"x", ""}); tag != nil {
|
|
data.kind1063Metadata.X = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"size", ""}); tag != nil {
|
|
data.kind1063Metadata.Size = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"dim", ""}); tag != nil {
|
|
data.kind1063Metadata.Dim = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"magnet", ""}); tag != nil {
|
|
data.kind1063Metadata.Magnet = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"i", ""}); tag != nil {
|
|
data.kind1063Metadata.I = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"blurhash", ""}); tag != nil {
|
|
data.kind1063Metadata.Blurhash = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"thumb", ""}); tag != nil {
|
|
data.kind1063Metadata.Thumb = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"image", ""}); tag != nil {
|
|
data.kind1063Metadata.Image = (*tag)[1]
|
|
data.image = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"summary", ""}); tag != nil {
|
|
data.kind1063Metadata.Summary = (*tag)[1]
|
|
}
|
|
case 30311:
|
|
data.templateId = LiveEvent
|
|
data.kind30311Metadata = &Kind30311Metadata{}
|
|
|
|
if tag := event.Tags.GetFirst([]string{"title", ""}); tag != nil {
|
|
data.kind30311Metadata.Title = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"summary", ""}); tag != nil {
|
|
data.kind30311Metadata.Summary = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"image", ""}); tag != nil {
|
|
data.kind30311Metadata.Image = (*tag)[1]
|
|
data.image = (*tag)[1]
|
|
}
|
|
if tag := event.Tags.GetFirst([]string{"status", ""}); tag != nil {
|
|
data.kind30311Metadata.Status = (*tag)[1]
|
|
}
|
|
pTags := event.Tags.GetAll([]string{"p", ""})
|
|
for _, p := range pTags {
|
|
if p[3] == "host" {
|
|
data.kind30311Metadata.Host = sdk.FetchProfileMetadata(ctx, pool, p[1], data.relays...)
|
|
data.kind30311Metadata.HostNpub = data.kind30311Metadata.Host.Npub()
|
|
}
|
|
}
|
|
tTags := event.Tags.GetAll([]string{"t", ""})
|
|
for _, t := range tTags {
|
|
data.kind30311Metadata.Tags = append(data.kind30311Metadata.Tags, t[1])
|
|
}
|
|
case 1311:
|
|
data.templateId = LiveEventMessage
|
|
data.kind1311Metadata = &Kind1311Metadata{}
|
|
data.content = event.Content
|
|
if atag := event.Tags.GetFirst([]string{"a", ""}); atag != nil {
|
|
parts := strings.Split((*atag)[1], ":")
|
|
kind, _ := strconv.Atoi(parts[0])
|
|
parentNevent, _ := nip19.EncodeEntity(parts[1], kind, parts[2], data.relays)
|
|
data.parentLink = template.HTML(replaceNostrURLsWithTags(nostrEveryMatcher, "nostr:"+parentNevent))
|
|
}
|
|
default:
|
|
data.templateId = Other
|
|
}
|
|
|
|
if event.Kind == 0 {
|
|
data.nprofile, _ = nip19.EncodeProfile(event.PubKey, limitAt(relays, 2))
|
|
data.metadata, _ = sdk.ParseMetadata(event)
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
|
|
defer cancel()
|
|
author, relays, _ := getEvent(ctx, data.npub, relaysForNip19)
|
|
if author != nil {
|
|
data.metadata, _ = sdk.ParseMetadata(author)
|
|
if data.metadata != nil {
|
|
data.authorLong = fmt.Sprintf("%s (%s)", data.metadata.Name, data.npub)
|
|
data.authorShort = fmt.Sprintf("%s (%s)", data.metadata.Name, data.npubShort)
|
|
}
|
|
}
|
|
if len(relays) > 0 {
|
|
data.nprofile, _ = nip19.EncodeProfile(event.PubKey, limitAt(relays, 2))
|
|
}
|
|
}
|
|
|
|
data.kindDescription = kindNames[event.Kind]
|
|
if data.kindDescription == "" {
|
|
data.kindDescription = fmt.Sprintf("Kind %d", event.Kind)
|
|
}
|
|
data.kindNIP = kindNIPs[event.Kind]
|
|
|
|
if event.Kind == 1063 {
|
|
if data.kind1063Metadata.IsImage() {
|
|
data.image = data.kind1063Metadata.URL
|
|
} else if data.kind1063Metadata.IsVideo() {
|
|
data.video = data.kind1063Metadata.URL
|
|
data.videoType = strings.Split(data.kind1063Metadata.M, "/")[1]
|
|
}
|
|
} else {
|
|
urls := urlMatcher.FindAllString(event.Content, -1)
|
|
for _, url := range urls {
|
|
switch {
|
|
case imageExtensionMatcher.MatchString(url):
|
|
if data.image == "" {
|
|
data.image = url
|
|
}
|
|
case videoExtensionMatcher.MatchString(url):
|
|
if data.video == "" {
|
|
data.video = url
|
|
if strings.HasSuffix(data.video, "mp4") {
|
|
data.videoType = "mp4"
|
|
} else if strings.HasSuffix(data.video, "mov") {
|
|
data.videoType = "mov"
|
|
} else {
|
|
data.videoType = "webm"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return data, nil
|
|
}
|