feat: implemented multiple preferred file (blossom) servers for user app data and meta

This commit is contained in:
Stixx 2024-12-30 17:25:42 +01:00
parent 62639f1986
commit 070e8c39f0
15 changed files with 458 additions and 222 deletions

View File

@ -1,6 +1,6 @@
<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"> <mxfile host="drawio-plugin" modified="2024-12-24T14:10:11.548Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" etag="j0ni0qfsydyzknoEy44Z" version="22.1.22" type="embed">
<diagram id="ADjf0_COJFV7FXKRQUJh" name="Page-1"> <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"> <mxGraphModel dx="1130" dy="824" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" background="#ffffff" math="0" shadow="0">
<root> <root>
<mxCell id="0" /> <mxCell id="0" />
<mxCell id="1" parent="0" /> <mxCell id="1" parent="0" />
@ -61,44 +61,119 @@
<mxCell id="31" value="Show suggested Blossom servers" style="whiteSpace=wrap;html=1;" parent="1" vertex="1"> <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" /> <mxGeometry x="665" y="250" width="120" height="60" as="geometry" />
</mxCell> </mxCell>
<mxCell id="33" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;rounded=0;" edge="1" parent="1"> <mxCell id="33" 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"> <mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="40" y="426" as="sourcePoint" /> <mxPoint x="40" y="426" as="sourcePoint" />
<mxPoint x="820" y="426" as="targetPoint" /> <mxPoint x="820" y="426" as="targetPoint" />
</mxGeometry> </mxGeometry>
</mxCell> </mxCell>
<mxCell id="39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="34" target="38"> <mxCell id="39" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="34" target="38" edge="1">
<mxGeometry relative="1" as="geometry" /> <mxGeometry relative="1" as="geometry" />
</mxCell> </mxCell>
<mxCell id="34" value="User 1" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxCell id="34" value="User 1" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="510" width="170" height="80" as="geometry" /> <mxGeometry x="40" y="620" width="170" height="80" as="geometry" />
</mxCell> </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"> <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;" parent="1" vertex="1">
<mxGeometry x="70" y="460" width="110" height="30" as="geometry" /> <mxGeometry x="70" y="570" width="110" height="30" as="geometry" />
</mxCell> </mxCell>
<mxCell id="42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="36" target="41"> <mxCell id="42" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="36" target="41" edge="1">
<mxGeometry relative="1" as="geometry" /> <mxGeometry relative="1" as="geometry" />
</mxCell> </mxCell>
<mxCell id="36" value="User 2" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxCell id="36" value="User 2" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="510" width="170" height="80" as="geometry" /> <mxGeometry x="320" y="620" width="170" height="80" as="geometry" />
</mxCell> </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"> <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;" parent="1" vertex="1">
<mxGeometry x="350" y="460" width="110" height="30" as="geometry" /> <mxGeometry x="350" y="570" width="110" height="30" as="geometry" />
</mxCell> </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"> <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;" parent="1" source="38" target="36" edge="1">
<mxGeometry relative="1" as="geometry" /> <mxGeometry relative="1" as="geometry" />
</mxCell> </mxCell>
<mxCell id="38" value="Creates document, sign and publish to A, B, C" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxCell id="38" value="Creates document, sign and publish to A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="660" width="170" height="80" as="geometry" /> <mxGeometry x="40" y="770" width="170" height="80" as="geometry" />
</mxCell> </mxCell>
<mxCell id="44" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="41" target="43"> <mxCell id="44" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="41" target="43" edge="1">
<mxGeometry relative="1" as="geometry" /> <mxGeometry relative="1" as="geometry" />
</mxCell> </mxCell>
<mxCell id="41" value="Reads the files From A, B, C" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxCell id="41" value="Reads the files From A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="660" width="170" height="80" as="geometry" /> <mxGeometry x="320" y="770" width="170" height="80" as="geometry" />
</mxCell> </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"> <mxCell id="43" value="Sign, update the meta.json and publish to D, E" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="810" width="170" height="80" as="geometry" /> <mxGeometry x="320" y="920" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="45" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; 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;File Servers (Blossom)&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
<mxGeometry x="330" y="440" width="190" height="30" as="geometry" />
</mxCell>
<mxCell id="46" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; 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;Sigits&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
<mxGeometry x="40" y="510" width="50" height="30" as="geometry" />
</mxCell>
<mxCell id="47" value="&lt;span style=&quot;color: rgb(0, 0, 0); font-family: Helvetica; font-size: 18px; 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;User App Data&lt;/span&gt;" style="text;whiteSpace=wrap;html=1;fontSize=18;" parent="1" vertex="1">
<mxGeometry x="40" y="1050" width="120" height="30" as="geometry" />
</mxCell>
<mxCell id="52" value="Loads the page" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="49" target="51" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="49" value="User" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="1290" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="50" 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;" parent="1" vertex="1">
<mxGeometry x="70" y="1240" width="110" height="30" as="geometry" />
</mxCell>
<mxCell id="61" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="51" target="60" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="51" value="Fetche the app data from A, B, C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="1450" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="64" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="58" target="63" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="58" value="List the sigits found in the app data" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="1750" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="62" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="60" target="58" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="60" value="We have 3 data sources, we merge all 3 sets into 1 object" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="1600" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="63" value="User opens a SIGIT (flow is on the top)" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="65" y="1920" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="65" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;text-decoration:underline;&quot;&gt;&lt;b&gt;interface UserAppData&lt;/b&gt;&lt;/p&gt;&lt;hr&gt;&lt;p style=&quot;margin: 0px 0px 0px 8px; font-size: 14px;&quot;&gt;sigits&lt;br style=&quot;border-color: var(--border-color); text-align: center;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;blossomVersions&lt;/span&gt;&lt;br style=&quot;border-color: var(--border-color); text-align: center;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;processedGiftWrapps&lt;/span&gt;&lt;br style=&quot;border-color: var(--border-color); text-align: center;&quot;&gt;&lt;span style=&quot;text-align: center;&quot;&gt;keyPair&lt;/span&gt;&lt;br&gt;&lt;/p&gt;" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" parent="1" vertex="1">
<mxGeometry x="40" y="1100" width="160" height="110" as="geometry" />
</mxCell>
<mxCell id="68" value="Creates a SIGIT" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="66" target="67" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="66" value="User" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="1290" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="70" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="67" target="69" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="67" value="Publish the SIGIT to A,B,C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="1450" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="72" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="1" source="69" target="71" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="69" value="Capture the SIGIT urls and publish them in UserAppData to the servers A,B,C" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="1610" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="74" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="71" target="73">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="71" value="Keep last&amp;nbsp; 10 versions of blossom which includes SIGIT" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="320" y="1770" width="170" height="80" as="geometry" />
</mxCell>
<mxCell id="79" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="73" target="77">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="73" value="Every blossom version can have multiple links that user added" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="345" y="1920" width="120" height="60" as="geometry" />
</mxCell>
<mxCell id="77" value="Get latest version, if it has multiple URLs choose the first one which matches the hash" style="ellipse;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="320" y="2060" width="170" height="80" as="geometry" />
</mxCell> </mxCell>
</root> </root>
</mxGraphModel> </mxGraphModel>

View File

@ -39,9 +39,13 @@ export const SignatureStrategy: MarkStrategy = {
if (await isOnline()) { if (await isOnline()) {
try { try {
const url = await uploadToFileStorage(file) const urls = await uploadToFileStorage(file)
console.info(`${file.name} uploaded to file storage`) console.info(
return url `${file.name} uploaded to following file storages: ${urls.join(', ')}`
)
// This bit was returning an url, and return of this function is being set to mark.value, so it kind of
// does not make sense to return an url to the file storage
return value
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
console.error( console.error(
@ -51,7 +55,7 @@ export const SignatureStrategy: MarkStrategy = {
} }
} }
} else { } else {
// TOOD: offline // TODO: offline
} }
return value return value

View File

@ -86,7 +86,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
}>({}) }>({})
const [markConfig, setMarkConfig] = useState<Mark[]>([]) const [markConfig, setMarkConfig] = useState<Mark[]>([])
const [title, setTitle] = useState<string>('') const [title, setTitle] = useState<string>('')
const [zipUrl, setZipUrl] = useState<string>('') const [zipUrls, setZipUrls] = useState<string[]>([])
const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{ const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
[signer: `npub1${string}`]: DocSignatureEvent [signer: `npub1${string}`]: DocSignatureEvent
@ -133,7 +133,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setId(id) setId(id)
setSig(sig) setSig(sig)
const { title, signers, viewers, fileHashes, markConfig, zipUrl } = const { title, signers, viewers, fileHashes, markConfig, zipUrls } =
await parseCreateSignatureEventContent(content) await parseCreateSignatureEventContent(content)
setTitle(title) setTitle(title)
@ -141,7 +141,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setViewers(viewers) setViewers(viewers)
setFileHashes(fileHashes) setFileHashes(fileHashes)
setMarkConfig(markConfig) setMarkConfig(markConfig)
setZipUrl(zipUrl) setZipUrls(zipUrls)
let encryptionKey: string | undefined let encryptionKey: string | undefined
if (meta.keys) { if (meta.keys) {
@ -322,7 +322,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
fileHashes, fileHashes,
markConfig, markConfig,
title, title,
zipUrl, zipUrls,
parsedSignatureEvents, parsedSignatureEvents,
completedAt, completedAt,
signedStatus, signedStatus,

View File

@ -55,7 +55,8 @@ import {
DEFAULT_TOOLBOX, DEFAULT_TOOLBOX,
settleAllFullfilfedPromises, settleAllFullfilfedPromises,
DEFAULT_LOOK_UP_RELAY_LIST, DEFAULT_LOOK_UP_RELAY_LIST,
uploadMetaToFileStorage uploadMetaToFileStorage,
isValidNip05
} from '../../utils' } from '../../utils'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import fileListStyles from '../../components/FileList/style.module.scss' import fileListStyles from '../../components/FileList/style.module.scss'
@ -264,8 +265,7 @@ export const CreatePage = () => {
// Otherwize if search already provided some results, user must manually click the search button // Otherwize if search already provided some results, user must manually click the search button
if (!foundUsers.length) { if (!foundUsers.length) {
// If it's NIP05 (includes @ or is a valid domain) send request to .well-known // If it's NIP05 (includes @ or is a valid domain) send request to .well-known
const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/ if (isValidNip05(userSearchInput)) {
if (domainRegex.test(userSearchInput)) {
setSearchUsersLoading(true) setSearchUsersLoading(true)
const pubkey = await handleSearchUserNip05(userSearchInput) const pubkey = await handleSearchUserNip05(userSearchInput)
@ -756,10 +756,10 @@ export const CreatePage = () => {
return null return null
} }
// Upload the file to the storage // Upload the file to the storage/s
const uploadFile = async ( const uploadFiles = async (
arrayBuffer: ArrayBuffer arrayBuffer: ArrayBuffer
): Promise<string | null> => { ): Promise<string[] | null> => {
const blob = new Blob([arrayBuffer]) const blob = new Blob([arrayBuffer])
// Create a File object with the Blob data // Create a File object with the Blob data
const file = new File([blob], `compressed-${unixNow()}.sigit`, { const file = new File([blob], `compressed-${unixNow()}.sigit`, {
@ -818,14 +818,14 @@ export const CreatePage = () => {
fileHashes: { fileHashes: {
[key: string]: string [key: string]: string
}, },
zipUrl: string zipUrls: string[]
) => { ) => {
const content: CreateSignatureEventContent = { const content: CreateSignatureEventContent = {
signers: signers.map((signer) => hexToNpub(signer.pubkey)), signers: signers.map((signer) => hexToNpub(signer.pubkey)),
viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
fileHashes, fileHashes,
markConfig, markConfig,
zipUrl, zipUrls,
title title
} }
@ -888,15 +888,15 @@ export const CreatePage = () => {
const markConfig = createMarks(fileHashes) const markConfig = createMarks(fileHashes)
setLoadingSpinnerDesc('Uploading files.zip to file storage') setLoadingSpinnerDesc('Uploading files.zip to file storages')
const fileUrl = await uploadFile(encryptedArrayBuffer) const fileUrls = await uploadFiles(encryptedArrayBuffer)
if (!fileUrl) return if (!fileUrls) return
setLoadingSpinnerDesc('Generating create signature') setLoadingSpinnerDesc('Generating create signature')
const createSignature = await generateCreateSignature( const createSignature = await generateCreateSignature(
markConfig, markConfig,
fileHashes, fileHashes,
fileUrl fileUrls
) )
if (!createSignature) return if (!createSignature) return
@ -934,11 +934,11 @@ export const CreatePage = () => {
const event = await updateUsersAppData(meta) const event = await updateUsersAppData(meta)
if (!event) return if (!event) return
const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey) const metaUrls = await uploadMetaToFileStorage(meta, encryptionKey)
setLoadingSpinnerDesc('Sending notifications to counterparties') setLoadingSpinnerDesc('Sending notifications to counterparties')
const promises = sendNotifications({ const promises = sendNotifications({
metaUrl, metaUrls,
keys: meta.keys keys: meta.keys
}) })
@ -964,7 +964,7 @@ export const CreatePage = () => {
const createSignature = await generateCreateSignature( const createSignature = await generateCreateSignature(
markConfig, markConfig,
fileHashes, fileHashes,
'' []
) )
if (!createSignature) return if (!createSignature) return

View File

@ -23,6 +23,7 @@ import {
getRelayInfo, getRelayInfo,
getRelayMap, getRelayMap,
hexToNpub, hexToNpub,
isValidRelayUri,
publishRelayMap, publishRelayMap,
shorten shorten
} from '../../../utils' } from '../../../utils'
@ -149,12 +150,7 @@ export const RelaysPage = () => {
const relayURI = `${webSocketPrefix}${newRelayURI?.trim().replace(webSocketPrefix, '')}` const relayURI = `${webSocketPrefix}${newRelayURI?.trim().replace(webSocketPrefix, '')}`
// Check if new relay URI is a valid string // Check if new relay URI is a valid string
if ( if (relayURI && !isValidRelayUri(relayURI)) {
relayURI &&
!/^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
relayURI
)
) {
if (relayURI !== webSocketPrefix) { if (relayURI !== webSocketPrefix) {
setNewRelayURIerror( setNewRelayURIerror(
'New relay URI is not valid. Example of valid relay URI: wss://sigit.relay.io' 'New relay URI is not valid. Example of valid relay URI: wss://sigit.relay.io'

View File

@ -22,7 +22,7 @@ import {
} from '../../../utils/file-servers.ts' } from '../../../utils/file-servers.ts'
import { useAppSelector } from '../../../hooks' import { useAppSelector } from '../../../hooks'
import { useDidMount } from '../../../hooks' import { useDidMount } from '../../../hooks'
import { SIGIT_BLOSSOM } from '../../../utils' import { isValidUrl } from '../../../utils'
import axios from 'axios' import axios from 'axios'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
@ -79,11 +79,7 @@ export const ServersPage = () => {
if (!serverURL) return if (!serverURL) return
// Check if new server is a valid URL // Check if new server is a valid URL
if ( if (!isValidUrl(serverURL)) {
!/^(https?:\/\/)(?!-)([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,6}$/gim.test(
serverURL
)
) {
if (serverURL !== protocol) { if (serverURL !== protocol) {
setNewRelayURLerror(errors.urlNotValid) setNewRelayURLerror(errors.urlNotValid)
return return
@ -109,10 +105,7 @@ export const ServersPage = () => {
} }
const handleDeleteServer = (serverURL: string) => { const handleDeleteServer = (serverURL: string) => {
if ( if (Object.keys(blossomServersMap).length === 1)
serverURL === SIGIT_BLOSSOM &&
Object.keys(blossomServersMap).length === 1
)
return serverRequirementWarning() return serverRequirementWarning()
// Remove server from the list // Remove server from the list
@ -213,6 +206,7 @@ export const ServersPage = () => {
<ServerItem <ServerItem
key={key} key={key}
serverURL={key} serverURL={key}
preventDelete={Object.keys(blossomServersMap).length === 1}
handleDeleteServer={handleDeleteServer} handleDeleteServer={handleDeleteServer}
/> />
))} ))}
@ -225,10 +219,15 @@ export const ServersPage = () => {
interface ServerItemProps { interface ServerItemProps {
serverURL: string serverURL: string
preventDelete?: boolean
handleDeleteServer?: (serverURL: string) => void handleDeleteServer?: (serverURL: string) => void
} }
const ServerItem = ({ serverURL, handleDeleteServer }: ServerItemProps) => { const ServerItem = ({
serverURL,
handleDeleteServer,
preventDelete
}: ServerItemProps) => {
return ( return (
<Box className={styles.server}> <Box className={styles.server}>
<List> <List>
@ -244,7 +243,7 @@ const ServerItem = ({ serverURL, handleDeleteServer }: ServerItemProps) => {
<Box <Box
onClick={() => handleDeleteServer && handleDeleteServer(serverURL)} onClick={() => handleDeleteServer && handleDeleteServer(serverURL)}
className={`${styles.leaveServerContainer} ${serverURL === SIGIT_BLOSSOM ? styles.disabled : ''}`} className={`${styles.leaveServerContainer} ${preventDelete ? styles.disabled : ''}`}
> >
<DeleteIcon /> <DeleteIcon />
<span>Remove</span> <span>Remove</span>

View File

@ -287,26 +287,42 @@ export const SignPage = () => {
return return
} }
const { zipUrl, encryptionKey } = res const { zipUrls, encryptionKey } = res
for (let i = 0; i < zipUrls.length; i++) {
const zipUrl = zipUrls[i]
const isLastZipUrl = i === zipUrls.length - 1
setLoadingSpinnerDesc('Fetching file from file server') setLoadingSpinnerDesc('Fetching file from file server')
axios
const res = await axios
.get(zipUrl, { .get(zipUrl, {
responseType: 'arraybuffer' responseType: 'arraybuffer'
}) })
.then((res) => { .catch((err) => {
console.error(
`error occurred in getting file from ${zipUrls}`,
err
)
toast.error(
err.message || `error occurred in getting file from ${zipUrls}`
)
return null
})
setIsLoading(false)
if (res) {
handleArrayBufferFromBlossom(res.data, encryptionKey) handleArrayBufferFromBlossom(res.data, encryptionKey)
setMeta(metaInNavState) setMeta(metaInNavState)
}) break
.catch((err) => { } else {
console.error(`error occurred in getting file from ${zipUrl}`, err) // No data returned, break from the loop
toast.error( if (isLastZipUrl) {
err.message || `error occurred in getting file from ${zipUrl}` break
) }
}) }
.finally(() => { }
setIsLoading(false)
})
} }
processSigit() processSigit()
@ -471,6 +487,10 @@ export const SignPage = () => {
setMeta(parsedMetaJson) setMeta(parsedMetaJson)
} }
/**
* Start the signing process
* When user signs, files will automatically be published to all user preferred servers
*/
const handleSign = async () => { const handleSign = async () => {
if (Object.entries(files).length === 0 || !meta) return if (Object.entries(files).length === 0 || !meta) return
@ -652,9 +672,9 @@ export const SignPage = () => {
return return
} }
let metaUrl: string let metaUrls: string[]
try { try {
metaUrl = await uploadMetaToFileStorage(meta, encryptionKey) metaUrls = await uploadMetaToFileStorage(meta, encryptionKey)
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
toast.error(error.message) toast.error(error.message)
@ -696,7 +716,10 @@ export const SignPage = () => {
setLoadingSpinnerDesc('Sending notifications') setLoadingSpinnerDesc('Sending notifications')
const users = Array.from(userSet) const users = Array.from(userSet)
const promises = users.map((user) => const promises = users.map((user) =>
sendNotification(npubToHex(user)!, { metaUrl, keys: meta.keys }) sendNotification(npubToHex(user)!, {
metaUrls: metaUrls,
keys: meta.keys
})
) )
await Promise.all(promises) await Promise.all(promises)
.then(() => { .then(() => {

View File

@ -216,7 +216,7 @@ export const VerifyPage = () => {
const { const {
submittedBy, submittedBy,
zipUrl, zipUrls,
encryptionKey, encryptionKey,
signers, signers,
viewers, viewers,
@ -376,7 +376,7 @@ export const VerifyPage = () => {
const users = Array.from(userSet) const users = Array.from(userSet)
const promises = users.map((user) => const promises = users.map((user) =>
sendNotification(npubToHex(user)!, { sendNotification(npubToHex(user)!, {
metaUrl, metaUrls: metaUrl,
keys: meta.keys! keys: meta.keys!
}) })
) )
@ -403,35 +403,55 @@ export const VerifyPage = () => {
const processSigit = async () => { const processSigit = async () => {
setIsLoading(true) setIsLoading(true)
// We have multiple zipUrls, we should fetch one by one and take the first one which successfully decrypts
// If file is altered decrytption will fail
setLoadingSpinnerDesc('Fetching file from file server') setLoadingSpinnerDesc('Fetching file from file server')
for (let i = 0; i < zipUrls.length; i++) {
const zipUrl = ''
const isLastZipUrl = i === zipUrls.length - 1
try { try {
// Fetch zip data
const res = await axios.get(zipUrl, { const res = await axios.get(zipUrl, {
responseType: 'arraybuffer' responseType: 'arraybuffer'
}) })
// Prepare file from response
const fileName = zipUrl.split('/').pop() const fileName = zipUrl.split('/').pop()
const file = new File([res.data], fileName!) const file = new File([res.data], fileName!)
const encryptedArrayBuffer = await file.arrayBuffer() const encryptedArrayBuffer = await file.arrayBuffer()
// Decrypt the array buffer
const arrayBuffer = await decryptArrayBuffer( const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer, encryptedArrayBuffer,
encryptionKey encryptionKey
).catch((err) => { ).catch((err) => {
console.log('err in decryption:>> ', err) console.error('Error in decryption:>> ', err)
toast.error(err.message || 'An error occurred in decrypting file.') toast.error(
return null err.message || 'An error occurred in decrypting file.'
)
return null // Continue iteration for next zipUrl
}) })
if (arrayBuffer) { if (!arrayBuffer) {
if (!isLastZipUrl) continue // Skip to next zipUrl if decryption fails
break // If last zipUrl break out of loop
}
// Load zip archive
const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => { const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => {
console.log('err in loading zip file :>> ', err) console.error('Error in loading zip file :>> ', err)
toast.error( toast.error(
err.message || 'An error occurred in loading zip file.' err.message || 'An error occurred in loading zip file.'
) )
return null return null // Skip to next zipUrl
}) })
if (!zip) return if (!zip) {
if (!isLastZipUrl) continue // Skip to next zipUrl
break // If last zipUrl break out of loop
}
const files: { [fileName: string]: SigitFile } = {} const files: { [fileName: string]: SigitFile } = {}
const fileHashes: { [key: string]: string | null } = {} const fileHashes: { [key: string]: string | null } = {}
@ -439,34 +459,30 @@ export const VerifyPage = () => {
(entry) => entry.name (entry) => entry.name
) )
// generate hashes for all entries in files folder of zipArchive // Generate hashes for all entries in the files folder of zipArchive
// these hashes can be used to verify the originality of files for (const entryFileName of fileNames) {
for (const fileName of fileNames) { const entryArrayBuffer = await readContentOfZipEntry(
const arrayBuffer = await readContentOfZipEntry(
zip, zip,
fileName, entryFileName,
'arraybuffer' 'arraybuffer'
) )
if (entryArrayBuffer) {
if (arrayBuffer) { files[entryFileName] = await convertToSigitFile(
files[fileName] = await convertToSigitFile( entryArrayBuffer,
arrayBuffer, entryFileName
fileName!
) )
const hash = await getHash(arrayBuffer) const hash = await getHash(entryArrayBuffer)
if (hash) { if (hash) {
fileHashes[fileName.replace(/^files\//, '')] = hash fileHashes[entryFileName.replace(/^files\//, '')] = hash
} }
} else { } else {
fileHashes[fileName.replace(/^files\//, '')] = null fileHashes[entryFileName.replace(/^files\//, '')] = null
} }
} }
setCurrentFileHashes(fileHashes) setCurrentFileHashes(fileHashes)
setFiles(files) setFiles(files)
setIsLoading(false) setIsLoading(false)
}
} catch (err) { } catch (err) {
const message = `error occurred in getting file from ${zipUrl}` const message = `error occurred in getting file from ${zipUrl}`
console.error(message, err) console.error(message, err)
@ -476,10 +492,11 @@ export const VerifyPage = () => {
setIsLoading(false) setIsLoading(false)
} }
} }
}
processSigit() processSigit()
} }
}, [encryptionKey, metaInNavState, zipUrl]) }, [encryptionKey, metaInNavState, zipUrls])
const handleVerify = async () => { const handleVerify = async () => {
if (!selectedFile) return if (!selectedFile) return

View File

@ -5,7 +5,7 @@ import { UserAppDataDispatchTypes } from './types'
const initialState: UserAppData = { const initialState: UserAppData = {
sigits: {}, sigits: {},
processedGiftWraps: [], processedGiftWraps: [],
blossomUrls: [] blossomVersions: []
} }
const reducer = ( const reducer = (

View File

@ -19,7 +19,6 @@ export interface Meta {
exportSignature?: string exportSignature?: string
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } } keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
timestamps?: OpenTimestamp[] timestamps?: OpenTimestamp[]
// TODO Add field: fileServers
} }
export interface CreateSignatureEventContent { export interface CreateSignatureEventContent {
@ -28,7 +27,7 @@ export interface CreateSignatureEventContent {
fileHashes: { [key: string]: string } fileHashes: { [key: string]: string }
markConfig: Mark[] markConfig: Mark[]
title: string title: string
zipUrl: string zipUrls: string[]
} }
export interface SignedEventContent { export interface SignedEventContent {
@ -76,9 +75,14 @@ export interface UserAppData {
*/ */
keyPair?: Keys keyPair?: Keys
/** /**
* Array for storing Urls for the files that stores all the sigits and processedGiftWraps on blossom. * Array for storing Urls for the files which stores all sigits and processedGiftWraps on file servers (blossom).
* We keep the last 10 versions
*/ */
blossomUrls: string[] blossomVersions: BlossomVersion[]
}
export interface BlossomVersion {
urls: string[]
} }
export interface DocSignatureEvent extends Event { export interface DocSignatureEvent extends Event {
@ -86,10 +90,10 @@ export interface DocSignatureEvent extends Event {
} }
export interface SigitNotification { export interface SigitNotification {
metaUrl: string metaUrls: string[]
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } } keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
} }
export function isSigitNotification(obj: unknown): obj is SigitNotification { export function isSigitNotification(obj: unknown): obj is SigitNotification {
return typeof (obj as SigitNotification).metaUrl === 'string' return typeof (obj as SigitNotification).metaUrls === 'object'
} }

View File

@ -6,7 +6,8 @@ export enum MetaStorageErrorType {
'FETCH_FAILED' = 'Fetching meta.json requires an encryption key.', 'FETCH_FAILED' = 'Fetching meta.json requires an encryption key.',
'HASH_VERIFICATION_FAILED' = 'Unable to verify meta.json.', 'HASH_VERIFICATION_FAILED' = 'Unable to verify meta.json.',
'DECRYPTION_FAILED' = 'Error decryping meta.json.', 'DECRYPTION_FAILED' = 'Error decryping meta.json.',
'UNHANDLED_FETCH_ERROR' = 'Unable to fetch meta.json. Something went wrong.' 'UNHANDLED_FETCH_ERROR' = 'Unable to fetch meta.json. Something went wrong.',
'NO_URLS_PROCESSED_SUCCESSFULLY' = 'No URLs were available to process.'
} }
export class MetaStorageError extends Error { export class MetaStorageError extends Error {

View File

@ -167,19 +167,27 @@ export const uploadMetaToFileStorage = async (
// Create the encrypted json file from array buffer and hash // Create the encrypted json file from array buffer and hash
const file = new File([encryptedArrayBuffer], `${hash}.json`) const file = new File([encryptedArrayBuffer], `${hash}.json`)
const url = await uploadToFileStorage(file) const urls = await uploadToFileStorage(file)
return url return urls
} }
/**
* Fetches the meta from one or more file storages, one by one, and it will take the first one, which has matching hash
* @param urls urls of meta files
* @param encryptionKey
*/
export const fetchMetaFromFileStorage = async ( export const fetchMetaFromFileStorage = async (
url: string, urls: string[],
encryptionKey: string | undefined encryptionKey: string | undefined
) => { ): Promise<Meta> => {
if (!encryptionKey) { if (!encryptionKey) {
throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED) throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
} }
for (let i = 0; i < urls.length; i++) {
const url = urls[i]
const isLastUrl = i === urls.length - 1
const encryptedArrayBuffer = await axios.get(url, { const encryptedArrayBuffer = await axios.get(url, {
responseType: 'arraybuffer' responseType: 'arraybuffer'
}) })
@ -189,16 +197,26 @@ export const fetchMetaFromFileStorage = async (
const urlHash = parts[parts.length - 1] const urlHash = parts[parts.length - 1]
const hash = await getHash(encryptedArrayBuffer.data) const hash = await getHash(encryptedArrayBuffer.data)
if (hash !== urlHash) { if (hash !== urlHash) {
throw new MetaStorageError(MetaStorageErrorType.HASH_VERIFICATION_FAILED) // If no more urls left to try and hash check failed, throw an error
if (isLastUrl)
throw new MetaStorageError(
MetaStorageErrorType.HASH_VERIFICATION_FAILED
)
// Otherwise, skip to the next url to fetch
continue
} }
const arrayBuffer = await decryptArrayBuffer( const arrayBuffer = await decryptArrayBuffer(
encryptedArrayBuffer.data, encryptedArrayBuffer.data,
encryptionKey encryptionKey
).catch((err) => { ).catch((err) => {
if (isLastUrl) {
throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, { throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, {
cause: err cause: err
}) })
} else {
return null
}
}) })
if (arrayBuffer) { if (arrayBuffer) {
@ -207,7 +225,14 @@ export const fetchMetaFromFileStorage = async (
const json = decoder.decode(arrayBuffer) const json = decoder.decode(arrayBuffer)
const meta = await parseJson<Meta>(json) const meta = await parseJson<Meta>(json)
return meta return meta
} else if (!isLastUrl) {
continue
} }
throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR) throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR)
}
throw new MetaStorageError(
MetaStorageErrorType.NO_URLS_PROCESSED_SUCCESSFULLY
)
} }

View File

@ -1,4 +1,4 @@
import axios from 'axios' import axios, { AxiosResponse } from 'axios'
import { import {
Event, Event,
EventTemplate, EventTemplate,
@ -11,7 +11,11 @@ import {
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { NostrController } from '../controllers' import { NostrController } from '../controllers'
import store from '../store/store' import store from '../store/store'
import { CreateSignatureEventContent, Meta } from '../types' import {
CreateSignatureEventContent,
FileServerPutResponse,
Meta
} from '../types'
import { hexToNpub, unixNow } from './nostr' import { hexToNpub, unixNow } from './nostr'
import { parseJson } from './string' import { parseJson } from './string'
import { hexToBytes } from '@noble/hashes/utils' import { hexToBytes } from '@noble/hashes/utils'
@ -19,18 +23,23 @@ import { getHash } from './hash.ts'
import { SIGIT_BLOSSOM } from './const.ts' import { SIGIT_BLOSSOM } from './const.ts'
/** /**
* Uploads a file to a file storage service. * Uploads a file to one or more file storage services.
* @param blob The Blob object representing the file to upload. * @param blob The Blob object representing the file to upload.
* @param nostrController The NostrController instance for handling authentication. * @param nostrController The NostrController instance for handling authentication.
* @returns The URL of the uploaded file. * @returns The array of URLs of the uploaded file.
*/ */
export const uploadToFileStorage = async (file: File) => { export const uploadToFileStorage = async (file: File): Promise<string[]> => {
// Define event metadata for authorization // Define event metadata for authorization
const hash = await getHash(await file.arrayBuffer()) const hash = await getHash(await file.arrayBuffer())
if (!hash) { if (!hash) {
throw new Error("Can't get file hash.") throw new Error("Can't get file hash.")
} }
const preferredServersMap = store.getState().servers.map || {}
const preferredServers = Object.keys(preferredServersMap)
// If no servers found, use SIGIT as fallback
if (!preferredServers.length) preferredServers.push(SIGIT_BLOSSOM)
const event: EventTemplate = { const event: EventTemplate = {
kind: 24242, kind: 24242,
content: 'Authorize Upload', content: 'Authorize Upload',
@ -54,16 +63,28 @@ export const uploadToFileStorage = async (file: File) => {
// Sign the authorization event using the dedicated key stored in user app data // Sign the authorization event using the dedicated key stored in user app data
const authEvent = finalizeEvent(event, hexToBytes(key)) const authEvent = finalizeEvent(event, hexToBytes(key))
// Upload the file to the file storage service using Axios const uploadPromises: Promise<AxiosResponse<FileServerPutResponse>>[] = []
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
// Upload the file to the file storage services using Axios
for (const preferredServer of preferredServers) {
const uploadPromise = axios.put<FileServerPutResponse>(
`${preferredServer}/upload`,
file,
{
headers: { headers: {
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
'Content-Type': 'application/sigit' // Set content type header 'Content-Type': 'application/sigit' // Set content type header
} }
}) }
)
// Return the URL of the uploaded file uploadPromises.push(uploadPromise)
return response.data.url as string }
const responses = await Promise.all(uploadPromises)
// Return the URLs of the uploaded files
return responses.map((response) => response.data.url) as string[]
} }
/** /**
@ -228,7 +249,7 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
if (!createSignatureContent) return null if (!createSignatureContent) return null
// Extract the ZIP URL from the create signature content // Extract the ZIP URL from the create signature content
const zipUrl = createSignatureContent.zipUrl const zipUrls = createSignatureContent.zipUrls
// Retrieve the user's public key from the state // Retrieve the user's public key from the state
const usersPubkey = store.getState().auth.usersPubkey! const usersPubkey = store.getState().auth.usersPubkey!
@ -259,7 +280,7 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
return { return {
createSignatureEvent, createSignatureEvent,
createSignatureContent, createSignatureContent,
zipUrl, zipUrls: zipUrls,
encryptionKey: decrypted encryptionKey: decrypted
} }
} }

View File

@ -1,5 +1,5 @@
import { bytesToHex, hexToBytes } from '@noble/hashes/utils' import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
import axios from 'axios' import axios, { AxiosResponse } from 'axios'
import _, { truncate } from 'lodash' import _, { truncate } from 'lodash'
import { import {
Event, Event,
@ -30,6 +30,8 @@ import {
import { Keys } from '../store/auth/types' import { Keys } from '../store/auth/types'
import store from '../store/store' import store from '../store/store'
import { import {
BlossomVersion,
FileServerPutResponse,
isSigitNotification, isSigitNotification,
Meta, Meta,
ProfileMetadata, ProfileMetadata,
@ -431,16 +433,16 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
// Return null if encrypted content retrieval fails // Return null if encrypted content retrieval fails
if (!encryptedContent) return null if (!encryptedContent) return null
// Handle case where the encrypted content is an empty object // Handle a case where the encrypted content is an empty object
if (encryptedContent === '{}') { if (encryptedContent === '{}') {
// Generate ephemeral key pair // Generate an ephemeral key pair
const secret = generateSecretKey() const secret = generateSecretKey()
const pubKey = getPublicKey(secret) const pubKey = getPublicKey(secret)
return { return {
sigits: {}, sigits: {},
processedGiftWraps: [], processedGiftWraps: [],
blossomUrls: [], blossomVersions: [],
keyPair: { keyPair: {
private: bytesToHex(secret), private: bytesToHex(secret),
public: pubKey public: pubKey
@ -466,6 +468,7 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
// Parse the decrypted content // Parse the decrypted content
const parsedContent = await parseJson<{ const parsedContent = await parseJson<{
blossomVersions: BlossomVersion[]
blossomUrls: string[] blossomUrls: string[]
keyPair: Keys keyPair: Keys
}>(decrypted).catch((err) => { }>(decrypted).catch((err) => {
@ -481,14 +484,23 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
// Return null if parsing fails // Return null if parsing fails
if (!parsedContent) return null if (!parsedContent) return null
const { blossomUrls, keyPair } = parsedContent // If old property blossomUrls is found, convert it to new appraoch blossomVersions
if (parsedContent.blossomUrls) {
parsedContent.blossomVersions = parsedContent.blossomUrls.map((url) => {
return {
urls: [url]
}
})
}
const { blossomVersions, keyPair } = parsedContent
// Return null if no blossom URLs are found // Return null if no blossom URLs are found
if (blossomUrls.length === 0) return null if (blossomVersions.length === 0) return null
// Fetch additional user app data from the first blossom URL // Fetch additional user app data from the last blossom version urls
const dataFromBlossom = await getUserAppDataFromBlossom( const dataFromBlossom = await getUserAppDataFromBlossom(
blossomUrls[0], blossomVersions[0],
keyPair.private keyPair.private
) )
@ -499,7 +511,7 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
// Return the final user application data // Return the final user application data
return { return {
blossomUrls, blossomVersions: blossomVersions,
keyPair, keyPair,
sigits, sigits,
processedGiftWraps processedGiftWraps
@ -543,9 +555,9 @@ export const updateUsersAppData = async (meta: Meta) => {
if (!isUpdated) return null if (!isUpdated) return null
const blossomUrls = [...appData.blossomUrls] const blossomVersions = [...appData.blossomVersions]
const newBlossomUrl = await uploadUserAppDataToBlossom( const newBlossomUrls = await uploadUserAppDataToBlossom(
sigits, sigits,
appData.processedGiftWraps, appData.processedGiftWraps,
appData.keyPair.private appData.keyPair.private
@ -560,21 +572,26 @@ export const updateUsersAppData = async (meta: Meta) => {
return null return null
}) })
if (!newBlossomUrl) return null if (!newBlossomUrls) return null
// insert new blossom url at the start of the array // insert new server (blossom) urls at the start of the array
blossomUrls.unshift(newBlossomUrl) blossomVersions.unshift({
urls: newBlossomUrls
})
// only keep last 10 blossom urls, delete older ones // only keep last 10 blossom versions (urls), delete older ones
if (blossomUrls.length > 10) { // Every version can be uploaded to multiple servers
const filesToDelete = blossomUrls.splice(10) if (blossomVersions.length > 10) {
filesToDelete.forEach((url) => { const versionsToDelete = blossomVersions.splice(10)
versionsToDelete.forEach((version) => {
for (const url of version.urls) {
deleteBlossomFile(url, appData.keyPair!.private).catch((err) => { deleteBlossomFile(url, appData.keyPair!.private).catch((err) => {
console.log( console.log(
'An error occurred in removing old file of user app data from blossom server', `An error occurred while removing an old file of user app data from the file server: ${url}`,
err err
) )
}) })
}
}) })
} }
@ -586,7 +603,7 @@ export const updateUsersAppData = async (meta: Meta) => {
.nip04Encrypt( .nip04Encrypt(
usersPubkey, usersPubkey,
JSON.stringify({ JSON.stringify({
blossomUrls, blossomVersions: blossomVersions,
keyPair: appData.keyPair keyPair: appData.keyPair
}) })
) )
@ -656,7 +673,7 @@ export const updateUsersAppData = async (meta: Meta) => {
store.dispatch( store.dispatch(
updateUserAppDataAction({ updateUserAppDataAction({
sigits, sigits,
blossomUrls, blossomVersions: blossomVersions,
processedGiftWraps: [...appData.processedGiftWraps], processedGiftWraps: [...appData.processedGiftWraps],
keyPair: { keyPair: {
...appData.keyPair ...appData.keyPair
@ -696,7 +713,7 @@ const deleteBlossomFile = async (url: string, privateKey: string) => {
} }
/** /**
* Function to upload user application data to the SIGit Blossom server. * Function to upload user application data to the user preferred File (Blossom) servers.
* @param sigits - An object containing metadata for the user application data. * @param sigits - An object containing metadata for the user application data.
* @param processedGiftWraps - An array of processed gift wrap IDs. * @param processedGiftWraps - An array of processed gift wrap IDs.
* @param privateKey - The private key used for encryption. * @param privateKey - The private key used for encryption.
@ -707,6 +724,11 @@ const uploadUserAppDataToBlossom = async (
processedGiftWraps: string[], processedGiftWraps: string[],
privateKey: string privateKey: string
) => { ) => {
const preferredServersMap = store.getState().servers.map || {}
const preferredServers = Object.keys(preferredServersMap)
// If no servers found, use SIGIT as fallback
if (!preferredServers.length) preferredServers.push(SIGIT_BLOSSOM)
// Create an object containing the sigits and processed gift wraps // Create an object containing the sigits and processed gift wraps
const obj = { const obj = {
sigits, sigits,
@ -752,31 +774,49 @@ const uploadUserAppDataToBlossom = async (
// Finalize the event with the private key // Finalize the event with the private key
const authEvent = finalizeEvent(event, hexToBytes(privateKey)) 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 uploadPromises: Promise<AxiosResponse<FileServerPutResponse>>[] = []
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
// Upload the file to the file storage services using Axios
for (const preferredServer of preferredServers) {
const uploadPromise = axios.put<FileServerPutResponse>(
`${preferredServer}/upload`,
file,
{
headers: { headers: {
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
} }
}) }
)
// Return the URL of the uploaded file uploadPromises.push(uploadPromise)
return response.data.url as string }
const responses = await Promise.all(uploadPromises)
// Return the URLs of the uploaded files
return responses.map((response) => response.data.url) as string[]
} }
/** /**
* Function to retrieve and decrypt user application data from Blossom server. * Function to retrieve and decrypt user application data from file (Blossom) servers.
* @param url - The URL to fetch the encrypted data from. * Since we pull from multiple servers, we will take the first one
* @param blossomVersion - The URL to fetch the encrypted data from.
* @param privateKey - The private key used for decryption. * @param privateKey - The private key used for decryption.
* @returns A promise that resolves to the decrypted and parsed user application data. * @returns A promise that resolves to the decrypted and parsed user application data.
*/ */
const getUserAppDataFromBlossom = async (url: string, privateKey: string) => { const getUserAppDataFromBlossom = async (
blossomVersion: BlossomVersion,
privateKey: string
) => {
// Initialize errorCode to track HTTP error codes // Initialize errorCode to track HTTP error codes
let errorCode = 0 let errorCode = 0
const blossomUrl = blossomVersion.urls[0]
// Fetch the encrypted data from the provided URL // Fetch the encrypted data from the provided URL
const encrypted = await axios const encrypted = await axios
.get(url, { .get(blossomUrl, {
responseType: 'blob' // Expect a blob response responseType: 'blob' // Expect a blob response
}) })
.then(async (res) => { .then(async (res) => {
@ -788,8 +828,13 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
}) })
.catch((err) => { .catch((err) => {
// Log and display an error message if the request fails // Log and display an error message if the request fails
console.error(`error occurred in getting file from ${url}`, err) console.error(
toast.error(err.message || `error occurred in getting file from ${url}`) `error occurred in getting file from ${blossomVersion}`,
err
)
toast.error(
err.message || `error occurred in getting file from ${blossomVersion}`
)
// Set errorCode to the HTTP status code if available // Set errorCode to the HTTP status code if available
if (err.request) { if (err.request) {
@ -950,7 +995,10 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
encryptionKey = decrypted encryptionKey = decrypted
} }
try { try {
meta = await fetchMetaFromFileStorage(notification.metaUrl, encryptionKey) meta = await fetchMetaFromFileStorage(
notification.metaUrls,
encryptionKey
)
} catch (error) { } catch (error) {
console.error(`An error occured fetching meta file from storage`, error) console.error(`An error occured fetching meta file from storage`, error)
return return

View File

@ -1,6 +1,7 @@
import { TimeoutError } from '../types/errors/TimeoutError.ts' import { TimeoutError } from '../types/errors/TimeoutError.ts'
import { CurrentUserFile } from '../types/file.ts' import { CurrentUserFile } from '../types/file.ts'
import { SigitFile } from './file.ts' import { SigitFile } from './file.ts'
import { NIP05_REGEX } from '../constants.ts'
export const debounceCustom = <T extends (...args: never[]) => void>( export const debounceCustom = <T extends (...args: never[]) => void>(
fn: T, fn: T,
@ -143,3 +144,25 @@ export const isPromiseRejected = <T>(
): result is PromiseRejectedResult => { ): result is PromiseRejectedResult => {
return result.status === 'rejected' return result.status === 'rejected'
} }
/**
* Checks if it's valid {protocol}{domain}
* @param url
*/
export const isValidUrl = (url: string) => {
return /^(https?:\/\/)(?!-)([A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,6}$/gim.test(url)
}
/**
* Checks if it's a valid domain or nip05 format
* @param value
*/
export const isValidNip05 = (value: string) => {
return NIP05_REGEX.test(value)
}
export const isValidRelayUri = (value: string) => {
return /^wss:\/\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}/.test(
value
)
}