feat: used sendDM method in contact form
This commit is contained in:
		
							
								
								
									
										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} | ||||
| @@ -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
									
								
							
							
						
						
									
										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