mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-17 14:24:27 +01:00
Add basic support for NIP-94 - File Metadata
This commit is contained in:
79
data.go
79
data.go
@@ -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,25 +234,34 @@ 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
|
||||||
for _, url := range urls {
|
if event.Kind == 1063 {
|
||||||
switch {
|
if strings.HasPrefix(kind1063Metadata["m"], "image") {
|
||||||
case imageExtensionMatcher.MatchString(url):
|
image = kind1063Metadata["url"]
|
||||||
if image == "" {
|
} else if strings.HasPrefix(kind1063Metadata["m"], "video") {
|
||||||
image = url
|
video = kind1063Metadata["url"]
|
||||||
}
|
videoType = strings.Split(kind1063Metadata["m"], "/")[1]
|
||||||
case videoExtensionMatcher.MatchString(url):
|
}
|
||||||
if video == "" {
|
} else {
|
||||||
video = url
|
urls := urlMatcher.FindAllString(event.Content, -1)
|
||||||
if strings.HasSuffix(video, "mp4") {
|
for _, url := range urls {
|
||||||
videoType = "mp4"
|
switch {
|
||||||
} else if strings.HasSuffix(video, "mov") {
|
case imageExtensionMatcher.MatchString(url):
|
||||||
videoType = "mov"
|
if image == "" {
|
||||||
} else {
|
image = url
|
||||||
videoType = "webm"
|
}
|
||||||
|
case videoExtensionMatcher.MatchString(url):
|
||||||
|
if video == "" {
|
||||||
|
video = url
|
||||||
|
if strings.HasSuffix(video, "mp4") {
|
||||||
|
videoType = "mp4"
|
||||||
|
} else if strings.HasSuffix(video, "mov") {
|
||||||
|
videoType = "mov"
|
||||||
|
} else {
|
||||||
|
videoType = "webm"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
58
pages.go
58
pages.go
@@ -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
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)) }}
|
||||||
|
|||||||
137
templates/file_metadata.html
Normal file
137
templates/file_metadata.html
Normal 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>
|
||||||
6
utils.go
6
utils.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user