feat: used sendDM method in contact form

This commit is contained in:
2024-06-19 11:49:56 +03:00
parent 36a1cad977
commit 99521fe8c7
8 changed files with 416 additions and 5 deletions

View 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
View File

@ -0,0 +1 @@
export * from './NostrController'

View File

@ -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}
@ -52,8 +64,35 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
<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 +104,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 +122,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 +142,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 +159,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
View File

@ -0,0 +1 @@
export * from './nostr'

14
src/types/nostr.ts Normal file
View 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
}