Compare commits
6 Commits
36a1cad977
...
publish-fi
Author | SHA1 | Date | |
---|---|---|---|
87ff818bf3 | |||
d3b1e4ec95 | |||
06d09aa91c | |||
c94ed82dba | |||
81f60a8c38 | |||
99521fe8c7 |
@@ -26,4 +26,4 @@ jobs:
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
surfer put --token ${{ secrets.SURFER_TOKEN }} --server website.datacontroller.io public/* /
|
||||
surfer put --token ${{ secrets.SURFER_TOKEN }} --server datacontroller.io public/* /
|
||||
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cSpell.words": ["Nostr"]
|
||||
}
|
15615
package-lock.json
generated
15615
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -17,34 +17,36 @@
|
||||
"lint:fix": "npx prettier --write \"src/**/*.+(ts|tsx|js|jsx|json|css|scss)\" --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@browniebroke/gatsby-image-gallery": "^6.2.0",
|
||||
"@mdx-js/mdx": "^1.6.22",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"gatsby": "^3.5.1",
|
||||
"gatsby-plugin-google-analytics": "^3.3.0",
|
||||
"@browniebroke/gatsby-image-gallery": "^8.2.0",
|
||||
"@mdx-js/mdx": "^3.0.1",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"gatsby": "^5.13.6",
|
||||
"gatsby-plugin-google-analytics": "^5.13.1",
|
||||
"gatsby-plugin-google-fonts": "^1.0.1",
|
||||
"gatsby-plugin-image": "^1.3.1",
|
||||
"gatsby-plugin-image": "^3.13.1",
|
||||
"gatsby-plugin-local-search": "^2.0.1",
|
||||
"gatsby-plugin-manifest": "^3.3.0",
|
||||
"gatsby-plugin-matomo": "0.13.0",
|
||||
"gatsby-plugin-react-helmet": "^4.3.0",
|
||||
"gatsby-plugin-sharp": "^3.3.1",
|
||||
"gatsby-plugin-sitemap": "^3.3.0",
|
||||
"gatsby-plugin-styled-components": "^4.3.0",
|
||||
"gatsby-remark-embed-video": "^3.1.1",
|
||||
"gatsby-remark-images": "^4.2.0",
|
||||
"gatsby-remark-responsive-iframe": "^4.2.1",
|
||||
"gatsby-source-filesystem": "^3.3.0",
|
||||
"gatsby-transformer-remark": "^4.0.0",
|
||||
"gatsby-transformer-sharp": "^3.3.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"gatsby-plugin-manifest": "^5.13.1",
|
||||
"gatsby-plugin-matomo": "0.16.2",
|
||||
"gatsby-plugin-react-helmet": "^6.13.1",
|
||||
"gatsby-plugin-sharp": "^5.13.1",
|
||||
"gatsby-plugin-sitemap": "^6.13.1",
|
||||
"gatsby-plugin-styled-components": "^6.13.1",
|
||||
"gatsby-remark-embed-video": "^3.2.1",
|
||||
"gatsby-remark-images": "^7.13.1",
|
||||
"gatsby-remark-responsive-iframe": "^6.13.1",
|
||||
"gatsby-source-filesystem": "^5.13.1",
|
||||
"gatsby-transformer-remark": "^6.13.1",
|
||||
"gatsby-transformer-sharp": "^5.13.1",
|
||||
"nostr-tools": "^2.7.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-icons": "^4.2.0",
|
||||
"react-share": "^4.4.0",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-share": "^5.1.0",
|
||||
"react-use-flexsearch": "^0.1.1",
|
||||
"styled-components": "^5.2.3"
|
||||
"styled-components": "^6.1.11",
|
||||
"tseep": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
|
@@ -318,7 +318,7 @@ export const Li = styled.li`
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
padding-right: 0.8rem !important;
|
||||
padding-left: 0.8rem !important;
|
||||
color: white;
|
||||
color: white !important;
|
||||
|
||||
&:before {
|
||||
${LinkUnderlineStyles}
|
||||
|
205
src/controllers/NostrController.ts
Normal file
205
src/controllers/NostrController.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
Event,
|
||||
EventTemplate,
|
||||
SimplePool,
|
||||
UnsignedEvent,
|
||||
finalizeEvent,
|
||||
nip04,
|
||||
nip19,
|
||||
verifyEvent,
|
||||
generateSecretKey,
|
||||
getPublicKey
|
||||
} from 'nostr-tools'
|
||||
import { EventEmitter } from 'tseep'
|
||||
import { SignedEvent, Keys } from '../types'
|
||||
|
||||
export class NostrController extends EventEmitter {
|
||||
private static instance: NostrController
|
||||
|
||||
private generatedKeys: Keys | undefined
|
||||
|
||||
private constructor() {
|
||||
super()
|
||||
|
||||
this.generatedKeys = this.generateKeys()
|
||||
}
|
||||
|
||||
public static getInstance(): NostrController {
|
||||
if (!NostrController.instance) {
|
||||
NostrController.instance = new NostrController()
|
||||
}
|
||||
return NostrController.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Function will publish provided event to the provided relays
|
||||
*/
|
||||
publishEvent = async (event: Event, relays: string[]) => {
|
||||
const simplePool = new SimplePool()
|
||||
const promises = simplePool.publish(relays, event)
|
||||
|
||||
const results = await Promise.allSettled(promises)
|
||||
|
||||
const publishedRelays: string[] = []
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') publishedRelays.push(relays[index])
|
||||
})
|
||||
|
||||
if (publishedRelays.length === 0) {
|
||||
const failedPublishes: any[] = []
|
||||
const fallbackRejectionReason =
|
||||
'Attempt to publish an event has been rejected with unknown reason.'
|
||||
|
||||
results.forEach((res, index) => {
|
||||
if (res.status === 'rejected') {
|
||||
failedPublishes.push({
|
||||
relay: relays[index],
|
||||
error: res.reason
|
||||
? res.reason.message || fallbackRejectionReason
|
||||
: fallbackRejectionReason
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
throw failedPublishes
|
||||
}
|
||||
|
||||
return publishedRelays
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs an event with private key (if it is present in local storage) or
|
||||
* with browser extension (if it is present) or
|
||||
* with nSecBunker instance.
|
||||
* @param event - unsigned nostr event.
|
||||
* @returns - a promised that is resolved with signed nostr event.
|
||||
*/
|
||||
signEvent = async (
|
||||
event: UnsignedEvent | EventTemplate
|
||||
): Promise<SignedEvent> => {
|
||||
if (!this.generatedKeys) {
|
||||
throw new Error(`Private & public key pair is not found.`)
|
||||
}
|
||||
|
||||
const { private: nsec } = this.generatedKeys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
const signedEvent = finalizeEvent(event, privateKey)
|
||||
|
||||
this.verifySignedEvent(signedEvent)
|
||||
|
||||
return Promise.resolve(signedEvent)
|
||||
}
|
||||
|
||||
nip04Encrypt = async (receiver: string, content: string) => {
|
||||
if (!this.generatedKeys) {
|
||||
throw new Error(`Private & public key pair is not found.`)
|
||||
}
|
||||
|
||||
const { private: nsec } = this.generatedKeys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
const encrypted = await nip04.encrypt(privateKey, receiver, content)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication.
|
||||
* @param fileUrl The URL of the encrypted zip file to be included in the DM.
|
||||
* @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM.
|
||||
* @param pubkey The public key of the recipient.
|
||||
* @param isSigner Boolean indicating whether the recipient is a signer or viewer.
|
||||
* @param setAuthUrl Function to set the authentication URL in the component state.
|
||||
*/
|
||||
sendDM = async (pubkey: string, message: string) => {
|
||||
// Set up timeout promise to handle encryption timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout occurred'))
|
||||
}, 60000) // Timeout duration = 60 seconds
|
||||
})
|
||||
|
||||
// Encrypt the DM content, with timeout
|
||||
const encrypted = await Promise.race([
|
||||
this.nip04Encrypt(this.npubToHex(pubkey) as string, message),
|
||||
timeoutPromise
|
||||
])
|
||||
|
||||
// Return if encryption failed
|
||||
if (!encrypted) throw new Error('Message was not encrypted.')
|
||||
|
||||
// Construct event metadata for the DM
|
||||
const event: EventTemplate = {
|
||||
kind: 4, // DM event type
|
||||
content: encrypted, // Encrypted DM content
|
||||
created_at: Math.floor(Date.now() / 1000), // Current timestamp
|
||||
tags: [['p', this.npubToHex(pubkey) as string]] // Tag with recipient's public key
|
||||
}
|
||||
|
||||
// Sign the DM event
|
||||
const signedEvent = await this.signEvent(event)
|
||||
|
||||
// Return if event signing failed
|
||||
if (!signedEvent) throw new Error('Message was not signed.')
|
||||
|
||||
// These relay will be used to send a DM. Recipient has to read from these relays to receive a DM.
|
||||
const relays = [
|
||||
'wss://relay.damus.io/',
|
||||
'wss://nos.lol/',
|
||||
'wss://relay.snort.social'
|
||||
]
|
||||
|
||||
// Publish the signed DM event to the recipient's read relays
|
||||
return await this.publishEvent(signedEvent, relays)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hexKey hex private or public key
|
||||
* @returns whether or not is key valid
|
||||
*/
|
||||
validateHex = (hexKey: string) => {
|
||||
return hexKey.match(/^[a-f0-9]{64}$/)
|
||||
}
|
||||
|
||||
/**
|
||||
* NPUB provided - it will convert NPUB to HEX
|
||||
* HEX provided - it will return HEX
|
||||
*
|
||||
* @param pubKey in NPUB, HEX format
|
||||
* @returns HEX format
|
||||
*/
|
||||
npubToHex = (pubKey: string): string | null => {
|
||||
// If key is NPUB
|
||||
if (pubKey.startsWith('npub1')) {
|
||||
try {
|
||||
return nip19.decode(pubKey).data as string
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// valid hex key
|
||||
if (this.validateHex(pubKey)) return pubKey
|
||||
|
||||
// Not a valid hex key
|
||||
return null
|
||||
}
|
||||
|
||||
generateKeys = (): Keys => {
|
||||
const nsec = generateSecretKey()
|
||||
|
||||
return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) }
|
||||
}
|
||||
|
||||
verifySignedEvent = (event: SignedEvent) => {
|
||||
const isGood = verifyEvent(event)
|
||||
|
||||
if (!isGood) {
|
||||
throw new Error(
|
||||
'Signed event did not pass verification. Check sig, id and pubkey.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
1
src/controllers/index.ts
Normal file
1
src/controllers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './NostrController'
|
@@ -1,5 +1,5 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import contactBg from '../images/contact_bg.jpg'
|
||||
|
||||
import '../styledComponents/contact.css'
|
||||
import { NostrController } from '../controllers'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
@@ -32,6 +33,17 @@ type DataProps = {
|
||||
}
|
||||
|
||||
const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const [name, setName] = useState<string>()
|
||||
const [email, setEmail] = useState<string>()
|
||||
const [subject, setSubject] = useState<string>()
|
||||
const [message, setMessage] = useState<string>()
|
||||
const [notification, setNotification] = useState<string>()
|
||||
|
||||
const getBorderStyle = (value: string | undefined) =>
|
||||
value === undefined ? {} : value ? {} : { border: '1px solid red' }
|
||||
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
@@ -51,9 +63,35 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
<StyledHeading>Contact Us</StyledHeading>
|
||||
|
||||
<form
|
||||
className="kwes-form"
|
||||
method="POST"
|
||||
action="https://kwes.io/api/foreign/forms/mxKuyK4lxZWnG2WNH3ga"
|
||||
onSubmit={async (evt) => {
|
||||
evt.preventDefault()
|
||||
|
||||
if (name && email && subject && message) {
|
||||
const res = await nostrController
|
||||
.sendDM(
|
||||
'npub1dc0000002dtkw7et06sztc9nvk79r6yju8gk69sr88rgrg0e8cvsnptgyv',
|
||||
`Name: ${name}
|
||||
Email: ${email}
|
||||
Subject: ${subject}
|
||||
Message: ${message}`
|
||||
)
|
||||
.catch((err) => {
|
||||
setNotification(
|
||||
`Something went wrong. Please check the console for more information. Please try one more time.`
|
||||
)
|
||||
|
||||
console.log(`Sending message error: `, err)
|
||||
})
|
||||
|
||||
if (res && res.length) {
|
||||
setNotification(`Message sent. We'll contact you shortly.`)
|
||||
} else {
|
||||
setNotification(
|
||||
`Something went wrong. Please try one more time.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="mb-3">
|
||||
<StyledLabel htmlFor="name" className="form-label">
|
||||
@@ -65,6 +103,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
id="name"
|
||||
name="name"
|
||||
rules="required|max:50"
|
||||
onChange={(evt) => {
|
||||
setName(evt.target.value)
|
||||
setNotification(undefined)
|
||||
}}
|
||||
style={getBorderStyle(name)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
@@ -78,6 +121,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
name="email"
|
||||
rules="required|email"
|
||||
aria-describedby="emailHelp"
|
||||
onChange={(evt) => {
|
||||
setEmail(evt.target.value)
|
||||
setNotification(undefined)
|
||||
}}
|
||||
style={getBorderStyle(email)}
|
||||
/>
|
||||
<div id="emailHelp" className="form-text">
|
||||
We'll never share your email with anyone else.
|
||||
@@ -93,6 +141,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
id="subject"
|
||||
name="subject"
|
||||
rules="required|max:50"
|
||||
onChange={(evt) => {
|
||||
setSubject(evt.target.value)
|
||||
setNotification(undefined)
|
||||
}}
|
||||
style={getBorderStyle(subject)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
@@ -105,12 +158,18 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
name="message"
|
||||
rows="5"
|
||||
rules="required|max:200"
|
||||
onChange={(evt) => {
|
||||
setMessage(evt.target.value)
|
||||
setNotification(undefined)
|
||||
}}
|
||||
style={getBorderStyle(message)}
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<SolidButton theme="dark">Submit</SolidButton>
|
||||
</div>
|
||||
</form>
|
||||
{notification && <span>{notification}</span>}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<ContactBackground src={contactBg} info="Book a Demo" />
|
||||
|
1
src/types/index.ts
Normal file
1
src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './nostr'
|
14
src/types/nostr.ts
Normal file
14
src/types/nostr.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface SignedEvent {
|
||||
kind: number
|
||||
tags: string[][]
|
||||
content: string
|
||||
created_at: number
|
||||
pubkey: string
|
||||
id: string
|
||||
sig: string
|
||||
}
|
||||
|
||||
export interface Keys {
|
||||
private: string
|
||||
public: string
|
||||
}
|
Reference in New Issue
Block a user