mirror of
https://git.v0l.io/florian/bouquet.git
synced 2025-03-17 18:13:00 +01:00
feat: Added experimental nip96 list support
This commit is contained in:
parent
832db79f7d
commit
2a10b83c73
@ -32,3 +32,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,6 +47,11 @@ const Server = ({
|
||||
<div className="flex flex-col grow">
|
||||
<div className="server-name">
|
||||
{server.name}
|
||||
<div
|
||||
className={`badge ${selectedServer == server.name ? 'badge-primary' : 'badge-neutral'} ml-2 align-middle`}
|
||||
>
|
||||
{server.type}
|
||||
</div>
|
||||
{server.isLoading && <span className="ml-2 loading loading-spinner loading-sm"></span>}
|
||||
</div>
|
||||
{server.isError ? (
|
||||
|
@ -56,7 +56,7 @@ export const ServerList = ({
|
||||
created_at: dayjs().unix(),
|
||||
content: '',
|
||||
pubkey: user?.pubkey || '',
|
||||
tags: newServers.map(s => ['server', `${s.url}`]),
|
||||
tags: newServers.filter(s => s.type == 'blossom').map(s => ['server', `${s.url}`]),
|
||||
});
|
||||
await ev.sign();
|
||||
console.log(ev.rawEvent());
|
||||
|
@ -72,13 +72,25 @@ const ServerListPopup: React.FC<ServerListPopupProps> = ({ isOpen, onClose, onSa
|
||||
{server.url} <div className="badge badge-neutral">{server.type}</div>
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => handleMoveUp(index)}>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
disabled={server.type != 'blossom'}
|
||||
onClick={() => handleMoveUp(index)}
|
||||
>
|
||||
<ArrowUpIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => handleMoveDown(index)}>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
disabled={server.type != 'blossom'}
|
||||
onClick={() => handleMoveDown(index)}
|
||||
>
|
||||
<ArrowDownIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-sm" onClick={() => handleDeleteServer(server.url)}>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
disabled={server.type != 'blossom'}
|
||||
onClick={() => handleDeleteServer(server.url)}
|
||||
>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -165,7 +165,9 @@ export const Transfer = () => {
|
||||
return transferSource ? (
|
||||
<>
|
||||
<ServerList
|
||||
servers={Object.values(serverInfo).filter(s => s.name == transferSource)}
|
||||
servers={Object.values(serverInfo)
|
||||
.filter(s => s.type == 'blossom')
|
||||
.filter(s => s.name == transferSource)}
|
||||
onCancel={() => closeTransferMode()}
|
||||
title={
|
||||
<>
|
||||
@ -175,6 +177,7 @@ export const Transfer = () => {
|
||||
></ServerList>
|
||||
<ServerList
|
||||
servers={Object.values(serverInfo)
|
||||
.filter(s => s.type == 'blossom')
|
||||
.filter(s => s.name != transferSource)
|
||||
.sort()}
|
||||
selectedServer={transferTarget}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BlobDescriptor, BlossomClient, SignedEvent } from 'blossom-client-sdk';
|
||||
import { useNDK } from '../utils/ndk';
|
||||
import { useServerInfo } from '../utils/useServerInfo';
|
||||
@ -207,14 +207,20 @@ function Upload() {
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
setFileEventsToPublish([]);
|
||||
setUploadStep(0);
|
||||
};
|
||||
|
||||
const [transfersInitialized, setTransfersInitialized] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
clearTransfers();
|
||||
setUploadStep(0);
|
||||
}, [servers]);
|
||||
if (servers.length > 0 && !transfersInitialized) {
|
||||
clearTransfers();
|
||||
setTransfersInitialized(true);
|
||||
}
|
||||
}, [servers, transfersInitialized]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -228,7 +234,7 @@ function Upload() {
|
||||
<div className="bg-base-200 rounded-xl p-4 text-neutral-content gap-4 flex flex-col">
|
||||
{uploadStep == 0 && (
|
||||
<UploadFileSelection
|
||||
servers={servers}
|
||||
servers={servers.filter(s => s.type == 'blossom')}
|
||||
transfers={transfers}
|
||||
setTransfers={setTransfers}
|
||||
cleanPrivateData={cleanPrivateData}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { BlobDescriptor, BlossomClient, EventTemplate, SignedEvent } from 'blossom-client-sdk';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const blossomUrlRegex = /https?:\/\/(?:www\.)?[^\s/]+\/([a-fA-F0-9]{64})(?:\.[a-zA-Z0-9]+)?/g;
|
||||
|
||||
export function extractHashesFromContent(text: string) {
|
||||
@ -15,3 +18,15 @@ export function extractHashFromUrl(url: string) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchBlossomList(
|
||||
serverUrl: string,
|
||||
pubkey: string,
|
||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>
|
||||
): Promise<BlobDescriptor[]> {
|
||||
const listAuthEvent = await BlossomClient.getListAuth(signEventTemplate, 'List Blobs');
|
||||
const blobs = await BlossomClient.listBlobs(serverUrl, pubkey!, undefined, listAuthEvent);
|
||||
|
||||
// fallback to deprecated created attibute for servers that are not using 'uploaded' yet
|
||||
return blobs.map(b => ({ ...b, uploaded: b.uploaded || b.created || dayjs().unix() }));
|
||||
}
|
||||
|
98
src/utils/nip96.ts
Normal file
98
src/utils/nip96.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { BlobDescriptor, EventTemplate, SignedEvent } from 'blossom-client-sdk';
|
||||
import { Server } from './useUserServers';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type MediaTransformation = 'resizing' | 'format_conversion' | 'compression' | 'metadata_stripping';
|
||||
|
||||
interface Plan {
|
||||
name: string;
|
||||
is_nip98_required: boolean;
|
||||
url: string;
|
||||
max_byte_size: number;
|
||||
file_expiration: [number, number];
|
||||
media_transformations: {
|
||||
image: MediaTransformation[];
|
||||
video: MediaTransformation[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Nip96ServerConfig {
|
||||
api_url: string;
|
||||
download_url: string;
|
||||
supported_nips: number[];
|
||||
tos_url: string;
|
||||
content_types: string[]; // MimeTypes
|
||||
plans: {
|
||||
[key: string]: Plan;
|
||||
};
|
||||
}
|
||||
|
||||
interface Nip96BlobDescriptor {
|
||||
tags: string[][];
|
||||
content: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
interface Nip96ListResponse {
|
||||
count: number; // server page size, eg. max(1, min(server_max_page_size, arg_count))
|
||||
total: number; // total number of files
|
||||
page: number; // the current page number
|
||||
files: Nip96BlobDescriptor[];
|
||||
}
|
||||
|
||||
export async function fetchNip96ServerConfig(serverUrl: string): Promise<Nip96ServerConfig> {
|
||||
const response = await fetch(serverUrl + '/.well-known/nostr/nip96.json');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const tenMinutesFromNow = () => dayjs().unix() + 10 * 60;
|
||||
|
||||
async function createNip98UploadAuthToken(
|
||||
url: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>
|
||||
): Promise<string> {
|
||||
const authEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
kind: 27235,
|
||||
content: '',
|
||||
tags: [
|
||||
['u', url],
|
||||
['method', method],
|
||||
['expiration', `${tenMinutesFromNow()}`],
|
||||
],
|
||||
};
|
||||
const signedEvent = await signEventTemplate(authEvent);
|
||||
console.log(JSON.stringify(signedEvent));
|
||||
return btoa(JSON.stringify(signedEvent));
|
||||
}
|
||||
|
||||
export async function fetchNip96List(
|
||||
server: Server,
|
||||
signEventTemplate: (template: EventTemplate) => Promise<SignedEvent>
|
||||
) {
|
||||
const page = 0;
|
||||
const count = 100;
|
||||
const baseUrl = server.nip96?.api_url || server.url;
|
||||
const listUrl = `${baseUrl}?page=${page}&count=${count}`;
|
||||
|
||||
const response = await fetch(listUrl, {
|
||||
headers: { Authorization: `Nostr ${await createNip98UploadAuthToken(listUrl, 'GET', signEventTemplate)}` },
|
||||
});
|
||||
|
||||
const list = (await response.json()) as Nip96ListResponse;
|
||||
|
||||
const getValueByTag = (tags: string[][], t: string) => tags.find(v => v[0] == t)?.[1];
|
||||
|
||||
return list.files.map(
|
||||
file =>
|
||||
({
|
||||
created: file.created_at,
|
||||
uploaded: file.created_at,
|
||||
type: getValueByTag(file.tags, 'm'),
|
||||
sha256: getValueByTag(file.tags, 'x'),
|
||||
size: parseInt(getValueByTag(file.tags, 'size') || '0', 10),
|
||||
url: getValueByTag(file.tags, 'url') || baseUrl + '/' + getValueByTag(file.tags, 'ox'),
|
||||
}) as BlobDescriptor
|
||||
);
|
||||
}
|
@ -5,6 +5,8 @@ import { useNDK } from '../utils/ndk';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Server, useUserServers } from './useUserServers';
|
||||
import dayjs from 'dayjs';
|
||||
import { fetchBlossomList } from './blossom';
|
||||
import { fetchNip96List } from './nip96';
|
||||
|
||||
export interface ServerInfo extends Server {
|
||||
virtual: boolean;
|
||||
@ -51,11 +53,12 @@ export const useServerInfo = () => {
|
||||
queries: servers.map(server => ({
|
||||
queryKey: ['blobs', server.name],
|
||||
queryFn: async () => {
|
||||
const listAuthEvent = await BlossomClient.getListAuth(signEventTemplate, 'List Blobs');
|
||||
const blobs = await BlossomClient.listBlobs(server.url, pubkey!, undefined, listAuthEvent);
|
||||
|
||||
// fallback to deprecated created attibute for servers that are not using 'uploaded' yet
|
||||
return blobs.map(b => ({ ...b, uploaded: b.uploaded || b.created || dayjs().unix() }));
|
||||
if (server.type === 'blossom') {
|
||||
return fetchBlossomList(server.url, pubkey!, signEventTemplate);
|
||||
} else if (server.type === 'nip96') {
|
||||
return fetchNip96List(server, signEventTemplate);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
enabled: !!pubkey && servers.length > 0,
|
||||
staleTime: Infinity,
|
||||
|
@ -4,6 +4,8 @@ import { nip19 } from 'nostr-tools';
|
||||
import { NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { USER_BLOSSOM_SERVER_LIST_KIND } from 'blossom-client-sdk';
|
||||
import useEvent from './useEvent';
|
||||
import { useQueries } from '@tanstack/react-query';
|
||||
import { Nip96ServerConfig, fetchNip96ServerConfig } from './nip96';
|
||||
|
||||
type ServerType = 'blossom' | 'nip96';
|
||||
|
||||
@ -11,13 +13,14 @@ export type Server = {
|
||||
type: ServerType;
|
||||
name: string;
|
||||
url: string;
|
||||
nip96?: Nip96ServerConfig;
|
||||
};
|
||||
|
||||
const USER_NIP96_SERVER_LIST_KIND = 10096;
|
||||
|
||||
export const useUserServers = (): Server[] => {
|
||||
const { user } = useNDK();
|
||||
|
||||
const { user } = useNDK();
|
||||
const pubkey = user?.npub && (nip19.decode(user?.npub).data as string); // TODO validate type
|
||||
|
||||
const blossomServerListEvent = useEvent(
|
||||
@ -30,25 +33,49 @@ export const useUserServers = (): Server[] => {
|
||||
{ disable: !pubkey }
|
||||
);
|
||||
|
||||
const servers = useMemo((): Server[] => {
|
||||
const serverUrls = [
|
||||
...(blossomServerListEvent?.getMatchingTags('server').map(t => t[1]) || []).map(s => ({
|
||||
url: s.toLocaleLowerCase().replace(/\/$/, ''),
|
||||
const blossomServers = useMemo((): Server[] => {
|
||||
return (blossomServerListEvent?.getMatchingTags('server').map(t => t[1]) || []).map(s => {
|
||||
const url = s.toLocaleLowerCase().replace(/\/$/, '');
|
||||
|
||||
return {
|
||||
url,
|
||||
name: url.replace(/https?:\/\//, ''),
|
||||
type: 'blossom' as ServerType,
|
||||
})),
|
||||
/* ...(nip96ServerListEvent?.getMatchingTags('server').map(t => t[1]) || []).map(s => ({
|
||||
url: s.toLocaleLowerCase().replace(/\/$/, ''),
|
||||
};
|
||||
});
|
||||
}, [blossomServerListEvent]);
|
||||
|
||||
const nip96Servers = useMemo((): Server[] => {
|
||||
return [
|
||||
/*...(nip96ServerListEvent?.getMatchingTags('server').map(t => t[1]) || []).map(s => {
|
||||
const url = s.toLocaleLowerCase().replace(/\/$/, '');
|
||||
|
||||
return {
|
||||
url,
|
||||
name: url.replace(/https?:\/\//, ''),
|
||||
type: 'nip96' as ServerType,
|
||||
})),*/
|
||||
};
|
||||
}),*/ {
|
||||
url: 'https://nostrcheck.me',
|
||||
name: 'nostrcheck.me',
|
||||
type: 'nip96' as ServerType,
|
||||
},
|
||||
];
|
||||
}, [nip96ServerListEvent]);
|
||||
|
||||
return serverUrls.map(s => ({
|
||||
type: s.type,
|
||||
name: s.url.replace(/https?:\/\//, ''),
|
||||
url: s.url,
|
||||
}));
|
||||
}, [blossomServerListEvent, nip96ServerListEvent]);
|
||||
const nip96InfoQueries = useQueries({
|
||||
queries: nip96Servers.map(server => ({
|
||||
queryKey: ['nip96info', server.url],
|
||||
queryFn: async () => await fetchNip96ServerConfig(server.url),
|
||||
})),
|
||||
});
|
||||
|
||||
const servers = useMemo((): Server[] => {
|
||||
return [
|
||||
...blossomServers,
|
||||
...nip96Servers.map((server, index) => ({ ...server, nip96: nip96InfoQueries[index].data })),
|
||||
];
|
||||
}, [blossomServers, nip96Servers, nip96InfoQueries]);
|
||||
|
||||
// console.log(servers);
|
||||
return servers;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user