Add basic support for NIP-94 - File Metadata

This commit is contained in:
Daniele Tonon
2023-10-30 23:29:23 +01:00
parent 783339defd
commit ad91d3b96b
6 changed files with 338 additions and 16 deletions

49
data.go
View File

@@ -85,6 +85,7 @@ type Data struct {
videoType string videoType string
image string image string
content string content string
kind1063Metadata map[string]string
} }
func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, error) { func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, error) {
@@ -118,6 +119,7 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e
authorRelays := []string{} authorRelays := []string{}
var content string var content string
var templateId TemplateID var templateId TemplateID
var kind1063Metadata map[string]string
eventRelays := []string{} eventRelays := []string{}
for _, relay := range relays { for _, relay := range relays {
@@ -172,6 +174,41 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e
original_nevent, _ := nip19.EncodeEvent((*reposted)[1], []string{}, "") original_nevent, _ := nip19.EncodeEvent((*reposted)[1], []string{}, "")
content = "Repost of nostr:" + original_nevent content = "Repost of nostr:" + original_nevent
} }
case 1063:
templateId = FileMetadata
kind1063Metadata = make(map[string]string)
keysToExtract := []string{
"url",
"m",
"aes-256-gcm",
"x",
"size",
"dim",
"magnet",
"i",
"blurhash",
"thumb",
"image",
"summary",
"alt",
}
for _, tag := range event.Tags {
if len(tag) == 2 {
key := tag[0]
value := tag[1]
// Check if the key is in the list of keys to extract
for _, k := range keysToExtract {
if key == k {
kind1063Metadata[key] = value
break
}
}
}
}
default: default:
if event.Kind >= 30000 && event.Kind < 40000 { if event.Kind >= 30000 && event.Kind < 40000 {
templateId = Other templateId = Other
@@ -197,10 +234,18 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e
} }
kindNIP := kindNIPs[event.Kind] kindNIP := kindNIPs[event.Kind]
urls := urlMatcher.FindAllString(event.Content, -1)
var image string var image string
var video string var video string
var videoType string var videoType string
if event.Kind == 1063 {
if strings.HasPrefix(kind1063Metadata["m"], "image") {
image = kind1063Metadata["url"]
} else if strings.HasPrefix(kind1063Metadata["m"], "video") {
video = kind1063Metadata["url"]
videoType = strings.Split(kind1063Metadata["m"], "/")[1]
}
} else {
urls := urlMatcher.FindAllString(event.Content, -1)
for _, url := range urls { for _, url := range urls {
switch { switch {
case imageExtensionMatcher.MatchString(url): case imageExtensionMatcher.MatchString(url):
@@ -220,6 +265,7 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e
} }
} }
} }
}
npubShort := npub[:8] + "…" + npub[len(npub)-4:] npubShort := npub[:8] + "…" + npub[len(npub)-4:]
authorLong := npub authorLong := npub
@@ -258,5 +304,6 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e
videoType: videoType, videoType: videoType,
image: image, image: image,
content: content, content: content,
kind1063Metadata: kind1063Metadata,
}, nil }, nil
} }

View File

@@ -17,6 +17,7 @@ const (
Note TemplateID = iota Note TemplateID = iota
LongForm LongForm
TelegramInstantView TelegramInstantView
FileMetadata
Other Other
) )
@@ -65,6 +66,10 @@ type DetailsPartial struct {
Kind int Kind int
KindNIP string KindNIP string
KindDescription string KindDescription string
Magnet string
Dim string
Size string
Summary string
} }
func (*DetailsPartial) TemplateText() string { return tmplDetails } func (*DetailsPartial) TemplateText() string { return tmplDetails }
@@ -241,6 +246,59 @@ type ProfilePage struct {
func (*ProfilePage) TemplateText() string { return tmplProfile } func (*ProfilePage) TemplateText() string { return tmplProfile }
var (
//go:embed templates/file_metadata.html
tmplFileMetadata string
FileMetadataTemplate = tmpl.MustCompile(&FileMetadataPage{})
)
type FileMetadataPage struct {
HeadCommonPartial `tmpl:"head_common"`
TopPartial `tmpl:"top"`
DetailsPartial `tmpl:"details"`
ClientsPartial `tmpl:"clients"`
FooterPartial `tmpl:"footer"`
AuthorLong string
Content template.HTML
CreatedAt string
Description string
Metadata nostr.ProfileMetadata
Npub string
NpubShort string
Oembed string
ParentLink template.HTML
Proxy string
SeenOn []string
Style string
Subject string
TextImageURL string
Title string
TitleizedContent string
TwitterTitle string
Video string
VideoType string
// Specific Metadata
Url string
M string
Aes256Gcm string
X string
Size string
Dim string
Magnet string
I string
Blurhash string
Thumb string
Image string
Summary string
Alt string
MType string // The first part of the mime type M
}
func (*FileMetadataPage) TemplateText() string { return tmplFileMetadata }
var ( var (
//go:embed templates/relay.html //go:embed templates/relay.html
tmplRelay string tmplRelay string

View File

@@ -231,6 +231,10 @@ func renderEvent(w http.ResponseWriter, r *http.Request) {
SeenOn: data.relays, SeenOn: data.relays,
Npub: data.npub, Npub: data.npub,
Nprofile: data.nprofile, Nprofile: data.nprofile,
Magnet: data.kind1063Metadata["magnet"],
Dim: data.kind1063Metadata["dim"],
Size: data.kind1063Metadata["size"],
Summary: data.kind1063Metadata["summary"],
} }
switch data.templateId { switch data.templateId {
@@ -280,6 +284,48 @@ func renderEvent(w http.ResponseWriter, r *http.Request) {
Video: data.video, Video: data.video,
VideoType: data.videoType, VideoType: data.videoType,
}) })
case FileMetadata:
thisImage := data.kind1063Metadata["image"]
if thisImage == "" && data.image != "" {
thisImage = data.image
}
err = FileMetadataTemplate.Render(w, &FileMetadataPage{
HeadCommonPartial: HeadCommonPartial{
IsProfile: false,
TailwindDebugStuff: tailwindDebugStuff,
NaddrNaked: data.naddrNaked,
NeventNaked: data.neventNaked,
},
DetailsPartial: detailsData,
ClientsPartial: ClientsPartial{
Clients: generateClientList(code, data.event),
},
AuthorLong: data.authorLong,
CreatedAt: data.createdAt,
Metadata: data.metadata,
Description: description,
Npub: data.npub,
NpubShort: data.npubShort,
Style: style,
Subject: subject,
TextImageURL: textImageURL,
Title: title,
TitleizedContent: titleizedContent,
TwitterTitle: twitterTitle,
Video: data.video,
VideoType: data.videoType,
Url: data.kind1063Metadata["url"],
M: data.kind1063Metadata["m"],
Aes256Gcm: data.kind1063Metadata["aes-256-gcm"],
X: data.kind1063Metadata["x"],
I: data.kind1063Metadata["i"],
Blurhash: data.kind1063Metadata["blurhash"],
Thumb: data.kind1063Metadata["thumb"],
Image: thisImage,
Alt: data.kind1063Metadata["alt"],
MType: strings.Split(data.kind1063Metadata["m"], "/")[0],
})
case Other: case Other:
err = OtherTemplate.Render(w, &OtherPage{ err = OtherTemplate.Render(w, &OtherPage{
HeadCommonPartial: HeadCommonPartial{ HeadCommonPartial: HeadCommonPartial{

View File

@@ -10,6 +10,34 @@
</div> </div>
{{ end }} {{ end }}
{{ if not (eq "" .Summary) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Summary</div>
{{.Summary}}
</div>
{{ end }}
{{ if not (eq "" .Dim) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Dimension</div>
{{.Dim}}
</div>
{{ end }}
{{ if not (eq "" .Size) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Size</div>
{{.Size}} bytes
</div>
{{ end }}
{{ if not (eq "" .Magnet) }}
<div class="mb-6 leading-5">
<div class="text-sm text-strongpink">Magnet URL</div>
{{.Magnet}}
</div>
{{ end }}
<!----> <!---->
{{ if not (eq 0 (len .SeenOn)) }} {{ if not (eq 0 (len .SeenOn)) }}

View File

@@ -0,0 +1,137 @@
<!doctype html>
<html class="theme--default text-lg font-light print:text-base sm:text-xl">
<meta charset="UTF-8" />
<head>
<title>{{.TitleizedContent}}</title>
<meta property="og:title" content="{{.Title}}" />
{{ if eq .Style "twitter" }}
<meta name="twitter:title" content="{{.TwitterTitle}}" />
{{ end }}
<meta property="og:site_name" content="{{.AuthorLong}}" />
{{ if not (eq "" .TextImageURL) }}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@nostrprotocol" />
<meta property="og:image" content="{{.TextImageURL}}" />
<meta name="twitter:image" content="{{.TextImageURL}}" />
{{ else }}
<!---->
<meta property="twitter:card" content="summary" />
{{ if not (eq "" .Image) }}
<meta property="og:image" content="{{.Image}}" />
<meta name="twitter:image" content="{{.Proxy}}{{.Image}}" />
{{ end }} {{ if not (eq "" .Video) }}
<meta property="og:video" content="{{.Video}}" />
<meta property="og:video:secure_url" content="{{.Video}}" />
<meta property="og:video:type" content="video/{{.VideoType}}" />
{{ end }} {{ end }}
<!---->
{{ if not (eq "" .Description) }}
<meta property="og:description" content="{{.Description}}" />
<meta name="twitter:description" content="{{.Description}}" />
{{ end }}
<!---->
{{ if not (eq "" .Oembed) }}
<link
rel="alternate"
type="application/json+oembed"
href="{{.Oembed}}&format=json"
/>
<link
rel="alternate"
type="text/xml+oembed"
href="{{.Oembed}}&format=xml"
/>
{{ end }}
<!---->
{{template "head_common" .HeadCommonPartial}}
</head>
<body
class="mb-16 bg-white text-gray-600 dark:bg-neutral-900 dark:text-neutral-50 print:text-black"
>
{{template "top" .}}
<div class="mx-auto px-4 sm:flex sm:items-center sm:justify-center sm:px-0">
<div
class="w-full max-w-screen-2xl justify-between gap-10 overflow-visible print:w-full sm:flex sm:w-9/12 sm:px-4 lg:gap-48vw"
>
<div class="w-full break-words print:w-full">
<header class="mb-4 max-w-full">
<a class="flex flex-wrap items-center" href="/{{.Npub}}">
<div
class="print:basis-1-12 imgclip mr-2 max-w-full basis-1/6 overflow-hidden sm:mr-4"
>
<img class="block h-auto w-full" src="{{.Metadata.Picture}}" />
</div>
<div class="block print:text-base sm:grow">
<div class="text-sm leading-4 sm:text-2xl">
{{.Metadata.Name}}
<!---->
{{if not (eq .Metadata.Name .Metadata.DisplayName)}}
<span class="text-sm text-stone-400 sm:text-xl"
>{{.Metadata.DisplayName}}</span
>
{{end}}
</div>
<div class="text-sm leading-4 text-stone-400 sm:text-base">
{{.NpubShort}}
</div>
</div>
</a>
</header>
<div class="w-full text-right text-sm text-stone-400">
{{.CreatedAt}}
</div>
<div class="w-full text-right text-sm text-stone-400">
{{ if not (eq "" .ParentLink) }} in reply to
<span class="text-strongpink">{{ .ParentLink }}</span> {{ end }}
</div>
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
<article
class="prose dark:prose-invert sm:prose-a:text-justify prose-headings:font-light prose-blockquote:mx-0 prose-blockquote:my-8 prose-blockquote:pl-4 prose-blockquote:pr-0 prose-blockquote:py-2 prose-blockquote:border-l-05rem prose-blockquote:border-solid prose-blockquote:border-l-gray-100 dark:prose-blockquote:border-l-zinc-800 prose-p:m-0 prose-p:mb-2 prose-cite:text-sm prose-ul:m-0 prose-ul:p-0 prose-ul:pl-4 prose-ol:m-0 prose-ol:p-0 prose-ol:pl-4 prose-li:mb-2 prose mb-6 leading-5"
>
{{ if (not (eq "" .Subject))}}
<h1 class="text-2xl">{{.Subject}}</h1>
{{ else }}
<h1 class="hidden">
{{.Metadata.Name}} on Nostr: {{.TitleizedContent}}
</h1>
{{ end }}
<!-- main content -->
{{ if (not (eq "" .Image))}}
<img src="{{ .Image }}" alt="{{ .Alt }}" />
{{ else if (eq "image" .MType)}}
<img src="{{ .Url }}" alt="{{ .Alt }}" />
{{ else if (eq "video" .MType)}}
<video controls width="100%%" class="max-h-[90vh] bg-neutral-300 dark:bg-zinc-700">
<source src="{{ .Url }}" alt="{{ .Alt }}"></video>
{{ end }}
<a href="{{ .Url }}" target="_new" class="block not-prose bg-strongpink mb-3 rounded-lg border-0 block basis-full px-4 text-[17px] font-normal text-white no-underline py-2 text-center font-light mx-auto w-2/6 text-center">Download file</a>
</article>
{{template "details" .DetailsPartial}}
<div
class="-ml-4 mb-6 h-1.5 w-1/3 bg-zinc-100 dark:bg-zinc-700 sm:-ml-2.5"
></div>
</div>
{{template "clients" .ClientsPartial}}
</div>
</div>
{{template "footer" .}}
</body>
</html>

View File

@@ -156,6 +156,12 @@ func generateClientList(code string, event *nostr.Event) []ClientReference {
{ID: "highlighter", Name: "Highlighter", URL: template.URL("https://highlighter.com/a/" + code)}, {ID: "highlighter", Name: "Highlighter", URL: template.URL("https://highlighter.com/a/" + code)},
{ID: "blogstack", Name: "Blogstack", URL: template.URL("https://blogstack.io/" + code)}, {ID: "blogstack", Name: "Blogstack", URL: template.URL("https://blogstack.io/" + code)},
} }
} else if event.Kind == 1063 {
return []ClientReference{
{ID: "native", Name: "Your native client", URL: template.URL("nostr:" + code)},
{ID: "snort", Name: "Snort", URL: template.URL("https://snort.social/p/" + code)},
{ID: "coracle", Name: "Coracle", URL: template.URL("https://coracle.social/" + code)},
}
} }
return nil return nil
} }