mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-17 06:14:22 +01:00
Add basic support for NIP-94 - File Metadata
This commit is contained in:
49
data.go
49
data.go
@@ -85,6 +85,7 @@ type Data struct {
|
||||
videoType string
|
||||
image string
|
||||
content string
|
||||
kind1063Metadata map[string]string
|
||||
}
|
||||
|
||||
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{}
|
||||
var content string
|
||||
var templateId TemplateID
|
||||
var kind1063Metadata map[string]string
|
||||
|
||||
eventRelays := []string{}
|
||||
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{}, "")
|
||||
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:
|
||||
if event.Kind >= 30000 && event.Kind < 40000 {
|
||||
templateId = Other
|
||||
@@ -197,10 +234,18 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e
|
||||
}
|
||||
kindNIP := kindNIPs[event.Kind]
|
||||
|
||||
urls := urlMatcher.FindAllString(event.Content, -1)
|
||||
var image string
|
||||
var video 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 {
|
||||
switch {
|
||||
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:]
|
||||
authorLong := npub
|
||||
@@ -258,5 +304,6 @@ func grabData(ctx context.Context, code string, isProfileSitemap bool) (*Data, e
|
||||
videoType: videoType,
|
||||
image: image,
|
||||
content: content,
|
||||
kind1063Metadata: kind1063Metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
58
pages.go
58
pages.go
@@ -17,6 +17,7 @@ const (
|
||||
Note TemplateID = iota
|
||||
LongForm
|
||||
TelegramInstantView
|
||||
FileMetadata
|
||||
Other
|
||||
)
|
||||
|
||||
@@ -65,6 +66,10 @@ type DetailsPartial struct {
|
||||
Kind int
|
||||
KindNIP string
|
||||
KindDescription string
|
||||
Magnet string
|
||||
Dim string
|
||||
Size string
|
||||
Summary string
|
||||
}
|
||||
|
||||
func (*DetailsPartial) TemplateText() string { return tmplDetails }
|
||||
@@ -241,6 +246,59 @@ type ProfilePage struct {
|
||||
|
||||
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 (
|
||||
//go:embed templates/relay.html
|
||||
tmplRelay string
|
||||
|
||||
@@ -231,6 +231,10 @@ func renderEvent(w http.ResponseWriter, r *http.Request) {
|
||||
SeenOn: data.relays,
|
||||
Npub: data.npub,
|
||||
Nprofile: data.nprofile,
|
||||
Magnet: data.kind1063Metadata["magnet"],
|
||||
Dim: data.kind1063Metadata["dim"],
|
||||
Size: data.kind1063Metadata["size"],
|
||||
Summary: data.kind1063Metadata["summary"],
|
||||
}
|
||||
|
||||
switch data.templateId {
|
||||
@@ -280,6 +284,48 @@ func renderEvent(w http.ResponseWriter, r *http.Request) {
|
||||
Video: data.video,
|
||||
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:
|
||||
err = OtherTemplate.Render(w, &OtherPage{
|
||||
HeadCommonPartial: HeadCommonPartial{
|
||||
|
||||
@@ -10,6 +10,34 @@
|
||||
</div>
|
||||
{{ 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)) }}
|
||||
|
||||
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: "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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user