feat: publish and read from relays a list of preferred file servers, run validation while adding new server

This commit is contained in:
Stixx 2024-12-17 21:08:51 +01:00
parent 092bb98670
commit 31d1630ab1
12 changed files with 626 additions and 2 deletions

106
docs/blossom-flow.drawio Normal file
View File

@ -0,0 +1,106 @@
<mxfile host="drawio-plugin" modified="2024-12-16T12:18:54.992Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" etag="AV-hpNJbeaKzNFoRwzLd" version="22.1.22" type="embed">
<diagram id="ADjf0_COJFV7FXKRQUJh" name="Page-1">
<mxGraphModel dx="942" dy="687" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="13" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="11" target="12" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="11" value="User login" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="30" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="15" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="12" target="14" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="12" value="Find all blossom servers provided by a user" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="150" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="14" value="Fetch all SIGITS from each blossom server and display them all" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="280" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="22" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="17" target="21" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="17" value="User opens a SIGIT" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="30" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="24" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="21" target="23" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="21" value="Display a blossom server where this SIGIT was found" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="345" y="150" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="23" value="Display other blossom servers where SIGIT is not found, and button which will publish to a blossom server" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="345" y="250" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="270" y="380" as="sourcePoint" />
<mxPoint x="270" y="30" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="27" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="580" y="380" as="sourcePoint" />
<mxPoint x="580" y="30" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="30" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="28" target="29" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="28" value="Settings page (settings/servers)" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="640" y="30" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="32" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="29" target="31" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="29" value="Allow user to add and remove Blossom servers" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="665" y="150" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="31" value="Show suggested Blossom servers" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="665" y="250" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="33" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" edge="1" parent="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="40" y="426" as="sourcePoint" />
<mxPoint x="820" y="426" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="34" target="38">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="34" value="User 1" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="40" y="510" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="35" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;&quot;&gt;Servers: A, B, C&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=15;" vertex="1" parent="1">
<mxGeometry x="70" y="460" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="36" target="41">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="36" value="User 2" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="320" y="510" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="37" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: center; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(251, 251, 251); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; float: none; display: inline !important;&quot;&gt;Servers: D, E&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=15;" vertex="1" parent="1">
<mxGeometry x="350" y="460" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="38" target="36">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="38" value="Creates document, sign and publish to A, B, C" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="40" y="660" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="44" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="41" target="43">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="41" value="Reads the files From A, B, C" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="320" y="660" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="43" value="Sign, update the meta.json and publish to D, E" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="320" y="810" width="170" height="80" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -169,3 +169,18 @@ li {
color: rgba(0, 0, 0, 0.25);
font-size: 14px;
}
.settings-container {
width: 100%;
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
grid-gap: 15px;
}
.text-center {
text-align: center;
}

View File

@ -2,6 +2,7 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle'
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
import CachedIcon from '@mui/icons-material/Cached'
import RouterIcon from '@mui/icons-material/Router'
import StorageIcon from '@mui/icons-material/Storage'
import { ListItem, useTheme } from '@mui/material'
import List from '@mui/material/List'
import ListItemIcon from '@mui/material/ListItemIcon'
@ -74,6 +75,12 @@ export const SettingsPage = () => {
</ListItemIcon>
{listItem('Relays')}
</ListItem>
<ListItem component={Link} to={appPrivateRoutes.servers}>
<ListItemIcon>
<StorageIcon />
</ListItemIcon>
{listItem('Servers')}
</ListItem>
<ListItem component={Link} to={appPrivateRoutes.cacheSettings}>
<ListItemIcon>
<CachedIcon />

View File

@ -0,0 +1,256 @@
import styles from './style.module.scss'
import { Container } from '../../../components/Container'
import { Footer } from '../../../components/Footer/Footer.tsx'
import {
Box,
Button,
CircularProgress,
InputAdornment,
List,
ListItem,
ListItemText,
TextField
} from '@mui/material'
import StorageIcon from '@mui/icons-material/Storage'
import DeleteIcon from '@mui/icons-material/Delete'
import { useState } from 'react'
import { toast } from 'react-toastify'
import { FileServerMap, KeyboardCode } from '../../../types'
import {
getFileServers,
publishFileServer
} from '../../../utils/file-servers.ts'
import { useAppSelector } from '../../../hooks/store.ts'
import { useDidMount } from '../../../hooks'
import { SIGIT_BLOSSOM } from '../../../utils'
import axios from 'axios'
import { cloneDeep } from 'lodash'
const protocol = 'https://'
const errors = {
urlNotValid:
'New server URL is not valid. Example of valid server URL: blossom.sigit.io'
}
export const ServersPage = () => {
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
const [newServerURL, setNewServerURL] = useState<string>('')
const [newRelayURLerror, setNewRelayURLerror] = useState<string>()
const [loadingServers, setLoadingServers] = useState<boolean>(true)
const [blossomServersMap, setBlossomServersMap] = useState<FileServerMap>({})
useDidMount(() => {
fetchFileServers()
})
const fetchFileServers = async () => {
if (usersPubkey) {
await getFileServers(usersPubkey).then((res) => {
if (res.map) {
if (Object.keys(res.map).length === 0) {
serverRequirementWarning()
}
setBlossomServersMap(res.map)
}
})
} else {
noUserKeyWarning()
}
setLoadingServers(false)
}
const noUserKeyWarning = () => toast.warning('No user key available.')
const serverRequirementWarning = () =>
toast.warning('At least one Blossom server is needed for SIGit to work.')
const handleAddNewServer = async () => {
if (!newServerURL.length) {
setNewRelayURLerror(errors.urlNotValid)
return
}
const serverURL = `${protocol}${newServerURL?.trim().replace(protocol, '')}`
if (!serverURL) return
// Check if new server is a valid URL
if (
!/^(https?:\/\/)(?!-)([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,6}$/gim.test(
serverURL
)
) {
if (serverURL !== protocol) {
setNewRelayURLerror(errors.urlNotValid)
return
}
}
if (Object.keys(blossomServersMap).includes(serverURL))
return toast.warning('This server is already added.')
const valid = await validateFileServer(serverURL).catch(() => null)
if (!valid)
return toast.warning(
`Server URL ${serverURL} does not seem to be a valid file server.`
)
setNewRelayURLerror('')
const tempBlossomServersMap = blossomServersMap
tempBlossomServersMap[serverURL] = { write: true, read: true }
setBlossomServersMap(tempBlossomServersMap)
setNewServerURL('')
publishFileServersList(tempBlossomServersMap)
}
const handleDeleteServer = (serverURL: string) => {
if (
serverURL === SIGIT_BLOSSOM &&
Object.keys(blossomServersMap).length === 1
)
return serverRequirementWarning()
// Remove server from the list
const tempBlossomServersMap = cloneDeep(blossomServersMap)
delete tempBlossomServersMap[serverURL]
setBlossomServersMap(tempBlossomServersMap)
// Publish new list to the relays
publishFileServersList(tempBlossomServersMap)
}
const publishFileServersList = (fileServersMap: FileServerMap) => {
if (!usersPubkey)
return toast.warning(
'No user key available, please reload and try again.'
)
publishFileServer(fileServersMap, usersPubkey)
.then((res) => {
toast.success(res)
})
.catch((err) => {
toast.error(err)
})
}
const handleInputKeydown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (
event.code === KeyboardCode.Enter ||
event.code === KeyboardCode.NumpadEnter
) {
event.preventDefault()
handleAddNewServer()
}
}
/**
* Checks if the file server is up and valid
* For now check will just include sending a GET request and checking if
* returned HTML includes word `Blossom`.
*
* Probably later, there will be appropriate sepc universal to all file servers
* which would include some kind of "check" endpoint.
* @param serverURL
*/
const validateFileServer = (serverURL: string) => {
return new Promise((resolve, reject) => {
axios
.get(serverURL)
.then((res) => {
if (res && res.data?.toLowerCase().includes('blossom server')) {
resolve(true)
} else {
reject(false)
}
})
.catch((err) => {
reject(err)
})
})
}
return (
<Container className={`settings-container ${styles.container}`}>
<Box className={styles.serverAddContainer}>
<TextField
label="Add new blossom server"
value={newServerURL}
onKeyDown={handleInputKeydown}
onChange={(e) => setNewServerURL(e.target.value)}
helperText={newRelayURLerror}
error={!!newRelayURLerror}
InputProps={{
startAdornment: (
<InputAdornment position="start">{protocol}</InputAdornment>
)
}}
className={styles.serverURItextfield}
/>
<Button variant="contained" onClick={() => handleAddNewServer()}>
Add
</Button>
</Box>
<Box className={styles.sectionTitle}>
<StorageIcon className={styles.sectionIcon} />
<span>YOUR BLOSSOM SERVERS</span>
</Box>
{loadingServers && (
<div className="text-center">
<CircularProgress />
</div>
)}
{blossomServersMap && (
<Box className={styles.serversContainer}>
{Object.keys(blossomServersMap).map((key) => (
<ServerItem
key={key}
serverURL={key}
handleDeleteServer={handleDeleteServer}
/>
))}
</Box>
)}
<Footer />
</Container>
)
}
interface ServerItemProps {
serverURL: string
handleDeleteServer?: (serverURL: string) => void
}
const ServerItem = ({ serverURL, handleDeleteServer }: ServerItemProps) => {
return (
<Box className={styles.server}>
<List>
<ListItem>
<span
className={[
styles.connectionStatus,
styles.connectionStatusConnected
].join(' ')}
/>
<ListItemText primary={serverURL} />
<Box
onClick={() => handleDeleteServer && handleDeleteServer(serverURL)}
className={`${styles.leaveServerContainer} ${serverURL === SIGIT_BLOSSOM ? styles.disabled : ''}`}
>
<DeleteIcon />
<span>Remove</span>
</Box>
</ListItem>
</List>
</Box>
)
}

View File

@ -0,0 +1,111 @@
@import '../../../styles/colors.scss';
.container {
color: $text-color;
.serverURItextfield {
width: 100%;
}
.serverAddContainer {
display: flex;
flex-direction: row;
gap: 10px;
width: 100%;
}
.sectionIcon {
font-size: 30px;
}
.sectionTitle {
margin-top: 35px;
margin-bottom: 10px;
display: flex;
flex-direction: row;
gap: 5px;
font-size: 1.5rem;
line-height: 2rem;
font-weight: 600;
}
.serversContainer {
display: flex;
flex-direction: column;
gap: 15px;
}
.server {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
.relayDivider {
margin-left: 10px;
margin-right: 10px;
}
.leaveServerContainer {
display: flex;
flex-direction: row;
gap: 10px;
cursor: pointer;
&.disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
.showInfo {
cursor: pointer;
}
.showInfoIcon {
margin-right: 3px;
margin-bottom: auto;
vertical-align: middle;
}
.relayInfoContainer {
display: flex;
flex-direction: column;
gap: 5px;
text-wrap: wrap;
}
.relayInfoTitle {
font-weight: 600;
}
.relayInfoSubTitle {
font-weight: 500;
}
.copyItem {
margin-left: 10px;
color: #34495e;
vertical-align: bottom;
cursor: pointer;
}
.connectionStatus {
border-radius: 9999px;
width: 10px;
height: 10px;
margin-right: 5px;
margin-top: 2px;
}
.connectionStatusConnected {
background-color: $relay-status-connected;
}
.connectionStatusNotConnected {
background-color: $relay-status-notconnected;
}
.connectionStatusUnknown {
background-color: $input-text-color;
}
}
}

View File

@ -8,6 +8,7 @@ export const appPrivateRoutes = {
profileSettings: '/settings/profile/:npub',
cacheSettings: '/settings/cache',
relays: '/settings/relays',
servers: '/settings/servers',
nostrLogin: '/settings/nostrLogin'
}

View File

@ -11,6 +11,7 @@ import { RelaysPage } from '../pages/settings/relays'
import { SettingsPage } from '../pages/settings/Settings'
import { SignPage } from '../pages/sign'
import { VerifyPage } from '../pages/verify'
import { ServersPage } from '../pages/settings/servers'
/**
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
@ -96,6 +97,10 @@ export const privateRoutes = [
path: appPrivateRoutes.relays,
element: <RelaysPage />
},
{
path: appPrivateRoutes.servers,
element: <ServersPage />
},
{
path: appPrivateRoutes.nostrLogin,
element: <NostrLoginPage />

View File

@ -19,6 +19,7 @@ export interface Meta {
exportSignature?: string
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
timestamps?: OpenTimestamp[]
// TODO Add field: fileServers
}
export interface CreateSignatureEventContent {

6
src/types/file-server.ts Normal file
View File

@ -0,0 +1,6 @@
export type FileServerMap = {
[key: string]: {
read: boolean
write: boolean
}
}

View File

@ -5,3 +5,4 @@ export * from './profile'
export * from './relay'
export * from './zip'
export * from './event'
export * from './file-server.ts'

115
src/utils/file-servers.ts Normal file
View File

@ -0,0 +1,115 @@
import { FileServerMap } from '../types'
import { NostrController, relayController } from '../controllers'
import { DEFAULT_LOOK_UP_RELAY_LIST, SIGIT_BLOSSOM } from './const.ts'
import { unixNow } from './nostr.ts'
import { Filter, UnsignedEvent, kinds } from 'nostr-tools'
/**
* Fetches the relays to get preferred file servers for the given npub
* @param npub hex pubkey
*/
const getFileServers = async (
npub: string
): Promise<{ map: FileServerMap; mapUpdated?: number }> => {
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/96.md
const eventFilter: Filter = {
kinds: [kinds.FileServerPreference],
authors: [npub]
}
const event = await relayController
.fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST)
.catch((err) => {
return Promise.reject(err)
})
if (event) {
// Handle found event 10096
const fileServersMap: FileServerMap = {}
const serverTags = event.tags.filter((tag) => tag[0] === 'server')
serverTags.forEach((tag) => {
const url = tag[1]
const serverType = tag[2]
// if 3rd element of server tag is undefined, server is WRITE and READ
fileServersMap[url] = {
write: serverType ? serverType === 'write' : true,
read: serverType ? serverType === 'read' : true
}
})
return Promise.resolve({
map: fileServersMap,
mapUpdated: event.created_at
})
} else {
return Promise.resolve({ map: getDefaultFileServerMap() })
}
}
/**
* Publishes a preferred file servers list for the given npub
* @param serverMap list of preferred servers
* @param npub hex pubkey
* @param extraRelaysToPublish additional relay on which to publish
*/
const publishFileServer = async (
serverMap: FileServerMap,
npub: string,
extraRelaysToPublish?: string[]
): Promise<string> => {
const timestamp = unixNow()
const serverURLs = Object.keys(serverMap)
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
const tags: string[][] = serverURLs.map((serverURL) => {
const serverTag = ['server', serverURL]
return serverTag.filter((value) => value !== '')
})
const newRelayMapEvent: UnsignedEvent = {
kind: kinds.FileServerPreference,
tags,
content: '',
pubkey: npub,
created_at: timestamp
}
const nostrController = NostrController.getInstance()
const signedEvent = await nostrController.signEvent(newRelayMapEvent)
let relaysToPublish = serverURLs
// Add extra relays if provided
if (extraRelaysToPublish) {
relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish]
}
// If a relay map is empty, use the most popular relay URIs
if (!relaysToPublish.length) {
relaysToPublish = DEFAULT_LOOK_UP_RELAY_LIST
}
const publishResult = await relayController.publish(
signedEvent,
relaysToPublish
)
if (publishResult && publishResult.length) {
return Promise.resolve(
`Preferred file servers published on: ${publishResult.join('\n')}`
)
}
return Promise.reject(
'Publishing updated preferred file servers was unsuccessful.'
)
}
const getDefaultFileServerMap = (): FileServerMap => ({
[SIGIT_BLOSSOM]: { write: true, read: true }
})
export { getFileServers, publishFileServer }

View File

@ -688,7 +688,7 @@ const deleteBlossomFile = async (url: string, privateKey: string) => {
}
/**
* Function to upload user application data to the Blossom server.
* Function to upload user application data to the SIGit Blossom server.
* @param sigits - An object containing metadata for the user application data.
* @param processedGiftWraps - An array of processed gift wrap IDs.
* @param privateKey - The private key used for encryption.
@ -744,7 +744,7 @@ const uploadUserAppDataToBlossom = async (
// Finalize the event with the private key
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
// TODO send to all added preferred blossom/file servers
// Upload the file to the file storage service using Axios
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
headers: {