├── .gitignore ├── LICENSE ├── README.md └── src ├── client ├── README.md ├── package.json ├── size-plugin.json ├── src │ ├── assets │ │ └── logo.webp │ ├── components │ │ ├── app.tsx │ │ └── header │ │ │ ├── index.tsx │ │ │ └── style.css │ ├── declaration.d.ts │ ├── index.ts │ ├── manifest.json │ ├── routes │ │ ├── home │ │ │ ├── index.tsx │ │ │ └── style.css │ │ └── register │ │ │ ├── index.tsx │ │ │ └── style.css │ ├── style │ │ └── index.css │ ├── sw.js │ └── template.html ├── tests │ ├── __mocks__ │ │ ├── browserMocks.ts │ │ ├── fileMocks.ts │ │ └── setupTests.ts │ ├── declarations.d.ts │ └── header.test.tsx ├── tsconfig.json └── yarn.lock └── server ├── index.ts ├── interfaces └── user.ts ├── lib ├── db.ts └── nostr.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | src/client/node_modules 4 | src/server/.env 5 | src/client/.env 6 | src/client/build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Serkan KOCAMAN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### What is NIP-05 for Nostr? 2 | In the NIP-05 nostr protocol, a human-readable identifier is used instead of a public key to describe users. 3 | 4 | This identifier is often described as something similar to an email address. 5 | 6 | For example, using an address like me@nostrprotocol.net allows your friends to easily find and follow you as a follower with this address. 7 | 8 | ### What is a Nostrprotocol.net Identifier? 9 | You can easily obtain a Nostrprotocol.net identifier for your nickname or name for free with Nostrprotocol.net. 10 | 11 | ### Website 12 | [https://nip05.nostrprotocol.net](https://nip05.nostrprotocol.net) 13 | 14 | 15 | ### Features 16 | --- 17 | - [x] Always free service. 18 | - [x] Fully compatible NIP-05 19 | - [x] Easy and comfortable. 20 | 21 | ### Contact & Support 22 | Please send a mail for any question. [nip05@nostrprotocol.net](mailto:nip05@nostrprotocol.net) 23 | 24 | ### Donate 25 | 26 | uprightbirth17@walletofsatoshi.com 27 | 28 | Serkan KOCAMAN by KiPSOFT 29 | -------------------------------------------------------------------------------- /src/client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## CLI Commands 4 | 5 | - `npm install`: Installs dependencies 6 | 7 | - `npm run dev`: Run a development, HMR server 8 | 9 | - `npm run serve`: Run a production-like server 10 | 11 | - `npm run build`: Production-ready build 12 | 13 | - `npm run lint`: Pass TypeScript files using ESLint 14 | 15 | - `npm run test`: Run Jest and Enzyme with 16 | [`enzyme-adapter-preact-pure`](https://github.com/preactjs/enzyme-adapter-preact-pure) for 17 | your tests 18 | 19 | For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md). 20 | -------------------------------------------------------------------------------- /src/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "client", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "cross-env preact build", 8 | "serve": "sirv build --cors --single", 9 | "dev": "preact watch --https", 10 | "lint": "eslint src", 11 | "test": "jest" 12 | }, 13 | "eslintConfig": { 14 | "parser": "@typescript-eslint/parser", 15 | "extends": [ 16 | "preact", 17 | "plugin:@typescript-eslint/recommended" 18 | ], 19 | "ignorePatterns": [ 20 | "build/" 21 | ] 22 | }, 23 | "dependencies": { 24 | "preact": "^10.10.0", 25 | "preact-render-to-string": "^5.2.1", 26 | "preact-router": "^3.2.1", 27 | "react-hook-form": "^7.43.0" 28 | }, 29 | "devDependencies": { 30 | "@types/enzyme": "^3.10.12", 31 | "@types/jest": "^27.4.1", 32 | "@typescript-eslint/eslint-plugin": "^5.30.6", 33 | "@typescript-eslint/parser": "^5.30.6", 34 | "cross-env": "^7.0.3", 35 | "enzyme": "^3.11.0", 36 | "enzyme-adapter-preact-pure": "^4.0.1", 37 | "eslint": "^8.20.0", 38 | "eslint-config-preact": "^1.3.0", 39 | "jest": "^27.5.1", 40 | "jest-preset-preact": "^4.0.5", 41 | "preact-cli": "^3.4.0", 42 | "sirv-cli": "^2.0.2", 43 | "typescript": "^4.5.2" 44 | }, 45 | "jest": { 46 | "preset": "jest-preset-preact", 47 | "setupFiles": [ 48 | "/tests/__mocks__/browserMocks.ts", 49 | "/tests/__mocks__/setupTests.ts" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/client/size-plugin.json: -------------------------------------------------------------------------------- 1 | [{"timestamp":1675551211223,"files":[{"filename":"ssr-build/ssr-bundle.22e5f.css","previous":1080,"size":0,"diff":-1080},{"filename":"ssr-build/ssr-bundle.js","previous":33910,"size":0,"diff":-33910},{"filename":"bundle.556e1.css","previous":0,"size":472,"diff":472},{"filename":"bundle.*****.esm.js","previous":0,"size":8513,"diff":8513},{"filename":"polyfills.*****.esm.js","previous":0,"size":2174,"diff":2174},{"filename":"route-home.chunk.01051.css","previous":0,"size":82,"diff":82},{"filename":"route-home.chunk.*****.esm.js","previous":0,"size":807,"diff":807},{"filename":"route-register.chunk.47ee6.css","previous":0,"size":487,"diff":487},{"filename":"route-register.chunk.*****.esm.js","previous":0,"size":13253,"diff":13253},{"filename":"sw-esm.js","previous":0,"size":11150,"diff":11150},{"filename":"sw.js","previous":0,"size":11148,"diff":11148},{"filename":"bundle.c19b2.js","previous":0,"size":8541,"diff":8541},{"filename":"polyfills.b3bd6.js","previous":0,"size":2272,"diff":2272},{"filename":"route-home.chunk.8d8dc.js","previous":0,"size":805,"diff":805},{"filename":"route-register.chunk.17973.js","previous":0,"size":14793,"diff":14793},{"filename":"index.html","previous":0,"size":1657,"diff":1657},{"filename":"200.html","previous":0,"size":607,"diff":607}]}] 2 | -------------------------------------------------------------------------------- /src/client/src/assets/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KiPSOFT/nostr-nip05-service/6659ee1d324776e0dc5227b5f8082b6c8e2b1794/src/client/src/assets/logo.webp -------------------------------------------------------------------------------- /src/client/src/components/app.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { Route, Router } from 'preact-router'; 3 | 4 | import Header from './header'; 5 | 6 | // Code-splitting is automated for `routes` directory 7 | import Home from '../routes/home'; 8 | import Register from '../routes/register'; 9 | 10 | const App = () => ( 11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 | ); 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/client/src/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { Link } from 'preact-router/match'; 3 | import style from './style.css'; 4 | 5 | const Header = () => ( 6 |
7 | 8 | Nostr Logo 9 |

Free Nostr NIP-05 Identifier Service

10 |
11 | 19 |
20 | ); 21 | 22 | export default Header; 23 | -------------------------------------------------------------------------------- /src/client/src/components/header/style.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | 6 | display: flex; 7 | justify-content: space-between; 8 | 9 | width: 100%; 10 | height: 3.5rem; 11 | 12 | background: lightslategrey; 13 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); 14 | z-index: 50; 15 | } 16 | 17 | .header a { 18 | display: inline-block; 19 | padding: 0 1rem; 20 | color: #fff; 21 | text-decoration: none; 22 | line-height: 3.5rem; 23 | } 24 | 25 | .header a:hover, 26 | .header a:active { 27 | background: rgba(0, 0, 0, 0.2); 28 | } 29 | 30 | .header a.logo { 31 | display: flex; 32 | align-items: center; 33 | padding: 0.5rem 1rem; 34 | } 35 | 36 | .logo h1 { 37 | padding: 0 0.5rem; 38 | font-size: 1.5rem; 39 | line-height: 2rem; 40 | font-weight: 400; 41 | } 42 | 43 | @media (max-width: 639px) { 44 | .logo h1 { 45 | display: none; 46 | } 47 | } 48 | 49 | .header nav a.active { 50 | background: rgba(0, 0, 0, 0.4); 51 | } 52 | -------------------------------------------------------------------------------- /src/client/src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const mapping: Record; 3 | export default mapping; 4 | } 5 | -------------------------------------------------------------------------------- /src/client/src/index.ts: -------------------------------------------------------------------------------- 1 | import './style/index.css'; 2 | import App from './components/app'; 3 | 4 | export default App; 5 | -------------------------------------------------------------------------------- /src/client/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get a NIP-05 identifier", 3 | "short_name": "NIP-05", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#fff", 8 | "theme_color": "#673ab8", 9 | "icons": [] 10 | } -------------------------------------------------------------------------------- /src/client/src/routes/home/index.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import style from './style.css'; 3 | 4 | const Home = () => { 5 | return ( 6 |
7 |

What is NIP-05 for Nostr?

8 |

9 | In the NIP-05 nostr protocol, a human-readable identifier is used instead of a public key to describe users. 10 | This identifier is often described as something similar to an email address. 11 | For example, using an address like me@nostrprotocol.net allows your friends to easily find and 12 | follow you as a follower with this address. 13 |

14 |

What is a Nostrprotocol.net Identifier?

15 |

16 | You can easily obtain a Nostrprotocol.net identifier for your nickname or name for free with Nostrprotocol.net. 17 | All you have to do is follow the steps below. 18 |

19 |

Steps to obtain an identifier

20 |
    21 |
  • First, open the registration page.
  • 22 |
  • Enter the public key value you are using on Nostr.
  • 23 |
  • Choose a prefix for your identifier. For example, if you choose 'nostr', 24 | your identifier will be 'nostr@nostrprotocol.net'. You must select a minimum of 4 characters.
  • 25 |
  • In this step, you need to send the direct message given to you through any Nostr client.
  • 26 |
  • The system will send you an automatic direct message confirming that the transaction has been 27 | approved after it has recognized the message you sent. Afterwards, you can enter your NIP-05 identifier 28 | through any Nostr client of your choice.
  • 29 |
30 |

Contact

31 |

32 | If you have any qu estions, you can send an email to nip05@nostrprotocol.net or 33 | send a message through nostr.

34 |
35 | ); 36 | }; 37 | 38 | export default Home; 39 | -------------------------------------------------------------------------------- /src/client/src/routes/home/style.css: -------------------------------------------------------------------------------- 1 | .home { 2 | margin-top: 3.5rem; 3 | } 4 | 5 | .home > ul { 6 | list-style-type: decimal; 7 | } -------------------------------------------------------------------------------- /src/client/src/routes/register/index.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import style from './style.css'; 3 | import { useForm } from "react-hook-form"; 4 | import { useRef, useState } from 'preact/hooks'; 5 | 6 | const Register = () => { 7 | const [ isSuccess, setIsSuccess ] = useState(false); 8 | const [ errMessage, setErrMessage ] = useState(''); 9 | const { register, handleSubmit, formState: { errors } } = useForm(); 10 | const copyTextRef = useRef(null); 11 | const [ buttonText, setButtonText ] = useState('Copy'); 12 | 13 | const onSubmit = async(data) => { 14 | const res = await fetch('/api/register', { 15 | method: 'POST', 16 | mode: 'cors', 17 | headers: { 18 | 'Content-Type': 'application/json' 19 | }, 20 | body: JSON.stringify(data) 21 | }); 22 | const json = await res.json(); 23 | if (res.status === 200) { 24 | setIsSuccess(true); 25 | } else { 26 | setErrMessage(json.message); 27 | } 28 | }; 29 | 30 | const handleCopyTextClick = async() => { 31 | await navigator.clipboard.writeText(copyTextRef.current.value); 32 | setButtonText('Copied'); 33 | } 34 | 35 | return ( 36 |
37 | {!isSuccess &&
38 |

Please fill in the below fields.

39 |
40 | 41 |
42 | 46 | 47 |
48 | {errors.name && {errors.name?.message ? errors.name?.message : 'Name is required.'}} 49 |
50 |
51 | 52 | 53 | {errors.publickey && Public key is required} 54 |
55 |
56 | 57 | 58 | {errors.email && E-mail is required} 59 |
60 | {!isSuccess && errMessage && {errMessage}} 61 |
62 | 63 |
64 |
} 65 | {isSuccess &&
66 |

Congratulations, your registration has been created. What you need to do now is:

67 |
    68 |
  • 69 |
    70 | Copy the following text. 71 |
    72 | 75 | 76 |
  • 77 |
  • Share the copied message from a client. Recommended clients; Damus (IOS), Amethyst (Android) Nostrgram (Web), iris (Web)
  • 78 |
  • Your request will be approved within 30 minutes and you will be notified with a direct message.
  • 79 |
80 |

Points to remember:

81 |
    82 |
  • Send the message exactly as it is.
  • 83 |
  • Make sure the message is sent from an account that matches the public key you provided during registration.
  • 84 |
85 |
} 86 |
87 | ); 88 | }; 89 | 90 | export default Register; 91 | -------------------------------------------------------------------------------- /src/client/src/routes/register/style.css: -------------------------------------------------------------------------------- 1 | .register { 2 | margin-top: 3.5rem; 3 | } 4 | 5 | .inputWrapper { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .page { 11 | width: 750px; 12 | margin: auto; 13 | padding-top: 10px; 14 | } 15 | 16 | form { 17 | margin-top: 58px; 18 | padding: 64px; 19 | 20 | background: #E5E5E5; 21 | border-radius: 20px 20px 20px 20px; 22 | min-height: 400px; 23 | 24 | display: flex; 25 | flex-direction: column; 26 | gap: 30px; 27 | } 28 | 29 | .inputWrapper input{ 30 | height: 56px; 31 | 32 | background: #FAFAFC; 33 | 34 | border: 1px solid #E6E6F0; 35 | border-radius: 8px; 36 | 37 | margin-bottom: 20px; 38 | color: black; 39 | font-size: 21px; 40 | } 41 | 42 | .inputWrapper label { 43 | font-size: 18px; 44 | line-height: 24px; 45 | 46 | color: #4E4958; 47 | 48 | padding-bottom: 8px; 49 | } 50 | 51 | .inputWrapper button { 52 | margin: auto; 53 | width: 40%; 54 | font-size: 20px; 55 | line-height: 26px; 56 | display: flex; 57 | justify-content: center; 58 | align-items: center; 59 | gap: 10px; 60 | padding: 15px 40px; 61 | margin: 52px auto; 62 | background: #FF5374; 63 | color:#FFFFFF ; 64 | border: none; 65 | border-radius: 20px; 66 | width: 600px; 67 | height:57px; 68 | cursor: pointer; 69 | } 70 | 71 | .inputWrapper button:hover { 72 | background: #cf3e5b; 73 | } 74 | 75 | .error { 76 | color: #cf3e5b; 77 | font-size: 15px; 78 | } 79 | 80 | .textarea { 81 | border-style: solid; 82 | border-width: 1px; 83 | border-color: black; 84 | background-color: white; 85 | color: black; 86 | font-size: 15px; 87 | width:100%; 88 | margin: 10px; 89 | } -------------------------------------------------------------------------------- /src/client/src/style/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: 'Helvetica Neue', arial, sans-serif; 3 | font-weight: 400; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | 7 | color-scheme: light dark; 8 | color: #444; 9 | background: #fafafa; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/src/sw.js: -------------------------------------------------------------------------------- 1 | import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/'; 2 | 3 | setupRouting(); 4 | setupPrecaching(getFiles()); 5 | -------------------------------------------------------------------------------- /src/client/src/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <% preact.title %> 6 | 7 | 8 | 9 | 10 | <% preact.headEnd %> 11 | 12 | 13 | <% preact.bodyEnd %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/client/tests/__mocks__/browserMocks.ts: -------------------------------------------------------------------------------- 1 | // Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage 2 | /** 3 | * An example how to mock localStorage is given below 👇 4 | */ 5 | 6 | /* 7 | // Mocks localStorage 8 | const localStorageMock = (function() { 9 | let store = {}; 10 | 11 | return { 12 | getItem: (key) => store[key] || null, 13 | setItem: (key, value) => store[key] = value.toString(), 14 | clear: () => store = {} 15 | }; 16 | 17 | })(); 18 | 19 | Object.defineProperty(window, 'localStorage', { 20 | value: localStorageMock 21 | }); */ 22 | -------------------------------------------------------------------------------- /src/client/tests/__mocks__/fileMocks.ts: -------------------------------------------------------------------------------- 1 | // This fixed an error related to the CSS and loading gif breaking my Jest test 2 | // See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets 3 | export default 'test-file-stub'; 4 | -------------------------------------------------------------------------------- /src/client/tests/__mocks__/setupTests.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-preact-pure'; 3 | 4 | configure({ 5 | adapter: new Adapter(), 6 | }); 7 | -------------------------------------------------------------------------------- /src/client/tests/declarations.d.ts: -------------------------------------------------------------------------------- 1 | // Enable enzyme adapter's integration with TypeScript 2 | // See: https://github.com/preactjs/enzyme-adapter-preact-pure#usage-with-typescript 3 | /// 4 | -------------------------------------------------------------------------------- /src/client/tests/header.test.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import Header from '../src/components/header'; 3 | // See: https://github.com/preactjs/enzyme-adapter-preact-pure 4 | import { shallow } from 'enzyme'; 5 | 6 | describe('Initial Test of the Header', () => { 7 | test('Header renders 3 nav items', () => { 8 | const context = shallow(
); 9 | expect(context.find('h1').text()).toBe('Preact App'); 10 | expect(context.find('Link').length).toBe(3); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "jsx": "react", 8 | "jsxFactory": "h", 9 | "jsxFragmentFactory": "Fragment", 10 | "noEmit": true, 11 | "allowJs": true, 12 | "checkJs": true, 13 | "skipLibCheck": true, 14 | "baseUrl": "./", 15 | "paths": { 16 | "react": ["./node_modules/preact/compat"], 17 | "react-dom": ["./node_modules/preact/compat"] 18 | } 19 | }, 20 | "include": ["src/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | import { Application, Router } from "https://deno.land/x/oak/mod.ts"; 2 | import { oakCors } from "https://deno.land/x/cors/mod.ts"; 3 | import { DB } from "./lib/db.ts"; 4 | import { load } from "https://deno.land/std/dotenv/mod.ts"; 5 | import NostrCheck from "./lib/nostr.ts"; 6 | 7 | await load({ 8 | export: true, 9 | allowEmptyValues: true, 10 | }); 11 | 12 | const db = new DB(); 13 | await db.connect(); 14 | 15 | const nostr = new NostrCheck(db); 16 | await nostr.connect(); 17 | 18 | const app = new Application(); 19 | 20 | const port = parseInt(Deno.env.get('PORT') || '9080'); 21 | 22 | const router = new Router(); 23 | 24 | router.get("/.well-known/nostr.json", async({request, response }: { request: any, response: any }) => { 25 | const name = request.url.searchParams.get('name'); 26 | const users = await db.getVerifiedUsers(name); 27 | const temp: any = {}; 28 | for (const usr of users) { 29 | temp[usr.name] = usr.publicKey.substring(0, 4) === 'npub' ? nostr.getKeyFromNip19(usr.publicKey) : usr.publicKey; 30 | } 31 | response.status = 200; 32 | response.body = { 33 | names: temp 34 | }; 35 | }); 36 | 37 | router.post('/api/register', async ({request, response}) => { 38 | const data = await request.body().value; 39 | if (!await db.findUser(data.publicKey)) { 40 | try { 41 | if (data.name.length < 4) { 42 | throw new Error('You can choose a name with at least 4 characters.'); 43 | } 44 | await db.registerUser({ 45 | createdAt: new Date(), 46 | email: data.email, 47 | name: data.name, 48 | publicKey: data.publicKey.substring(0, 4) === 'npub' ? nostr.getKeyFromNip19(data.publicKey) : data.publicKey, 49 | verified: false 50 | }); 51 | response.status = 200; 52 | response.body = { 53 | message: 'Success' 54 | }; 55 | } catch (err: any) { 56 | response.status = 500; 57 | response.body = { 58 | message: err.message 59 | }; 60 | } 61 | } else { 62 | response.status = 400; 63 | response.body = { 64 | message: 'This public key already exists.' 65 | } 66 | } 67 | }); 68 | 69 | app.use(oakCors()); 70 | app.use(router.routes()); 71 | app.use(router.allowedMethods()); 72 | 73 | console.log('running on port ', port); 74 | await app.listen({ port }); -------------------------------------------------------------------------------- /src/server/interfaces/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | _id?: any, 3 | name: string; 4 | email?: string; 5 | publicKey: string; 6 | verified: boolean; 7 | createdAt: Date; 8 | verifiedAt?: Date; 9 | } -------------------------------------------------------------------------------- /src/server/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../interfaces/user.ts"; 2 | import { MongoClient } from "https://deno.land/x/mongo@v0.31.1/mod.ts"; 3 | import moment from "https://deno.land/x/momentjs@2.29.1-deno/mod.ts"; 4 | 5 | export class DB { 6 | public db: any; 7 | 8 | async connect() { 9 | const mongoDBUri = `mongodb+srv://${Deno.env.get( 10 | "MONGODB_USER" 11 | )}:${Deno.env.get("MONGODB_PASSWORD")}@${Deno.env.get( 12 | "MONGODB_HOST" 13 | )}/nip05?authMechanism=SCRAM-SHA-1`; 14 | console.log(`DB Connecting to; ${mongoDBUri} `); 15 | const client = new MongoClient(); 16 | await client.connect(mongoDBUri); 17 | this.db = client.database("nip05"); 18 | } 19 | 20 | async registerUser(user: User) { 21 | return await this.db.collection("users").insertOne(user); 22 | } 23 | 24 | async findUser(publicKey: string) { 25 | return await this.db.collection("users").findOne({ publicKey }); 26 | } 27 | 28 | async getVerifiedUsers(name: string): Promise> { 29 | let search: any = { 30 | verified: true, 31 | }; 32 | if (name) { 33 | search.name = name; 34 | } 35 | return await this.db.collection("users").find(search).toArray(); 36 | } 37 | 38 | async getNonVerifiedUsers(): Promise> { 39 | let search: any = { 40 | verified: false, 41 | createdAt: { 42 | $gte: moment(new Date()).subtract(1, 'months') 43 | } 44 | }; 45 | return await this.db 46 | .collection("users") 47 | .find(search) 48 | .toArray(); 49 | } 50 | 51 | async checkUser(publicKey: string): Promise { 52 | return await this.db.collection("users").findOne({ 53 | publicKey, 54 | verified: false, 55 | }); 56 | } 57 | 58 | async updateUser(usr: User) { 59 | return await this.db.collection("users").updateOne( 60 | { 61 | _id: usr._id, 62 | }, 63 | { $set: usr } 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/server/lib/nostr.ts: -------------------------------------------------------------------------------- 1 | import { Nostr } from 'https://deno.land/x/nostr_deno_client@v0.2.7/mod.ts'; 2 | import { User } from '../interfaces/user.ts'; 3 | import { DB } from './db.ts'; 4 | 5 | export default class NostrCheck extends Nostr { 6 | private intervalId: number = 0; 7 | private db: DB; 8 | 9 | constructor(_db: DB) { 10 | super(); 11 | this.db = _db; 12 | this.privateKey = Deno.env.get('PRIVATE_KEY'); 13 | this.relayList.push({ 14 | name: 'Damus', 15 | url: 'wss://relay.damus.io' 16 | } as never); 17 | this.relayList.push({ 18 | name: 'Snort', 19 | url: 'wss://relay.snort.social' 20 | } as never); 21 | this.relayList.push({ 22 | name: 'Wellorder', 23 | url: 'wss://relay.wellorder.net' 24 | } as never); 25 | this.on('relayConnected', this.eventRelayConnected.bind(this), null); 26 | this.on('relayError', (err: any) => console.log('Relay error;', err), null); 27 | } 28 | 29 | eventRelayConnected() { 30 | console.log('Relay connected.'); 31 | if (!this.intervalId) { 32 | this.intervalId = setInterval(this.checkUsers.bind(this), 1000); 33 | } 34 | } 35 | 36 | private getHoursAgo(hours: number): number { 37 | return Math.floor(Date.now() / 1000) - (hours * 60 * 60); 38 | } 39 | 40 | private async verifyUser(publicKey: string, usr: User) { 41 | usr.verified = true; 42 | usr.verifiedAt = new Date(); 43 | await this.db.updateUser(usr); 44 | await this.sendMessage(publicKey, `🍺 Congratulations. Your NIP-05 registration request for nostrprotocol.net has been approved. 45 | You can now enter the information ${usr.name}@nostrprotocol.net from your nostr client's profile settings 46 | into the NIP-05 field and start using it. `); 47 | } 48 | 49 | async checkUsers() { 50 | try { 51 | clearInterval(this.intervalId); 52 | const since = this.getHoursAgo(48); 53 | console.log('Checking users...', Date.now(), since); 54 | const filter = { kinds: [1], since, '#p': [Deno.env.get('PUBLIC_KEY')] }; 55 | const events = await this.filter(filter).collect(); 56 | console.log('Events ...', events); 57 | for (const evnt of events) { 58 | const usr = await this.db.checkUser(evnt.pubkey); 59 | if (usr && evnt.content === 'Please approve my NIP-05 request on https://nip05.nostprotocol.net #[0]') { 60 | await this.verifyUser(evnt.pubkey, usr); 61 | console.log(`${usr.name} request is approved.`); 62 | } 63 | } 64 | console.log('Receiving messages sent with "nostr" clients that do not support mention feature...') 65 | const _users = await this.db.getNonVerifiedUsers(); 66 | for (const usr of _users) { 67 | const _filter = { kinds: [1], since, authors: [ usr.publicKey ] }; 68 | console.log(`User checking... ${usr.publicKey}`); 69 | const _events = await this.filter(_filter).collect(); 70 | console.log('Events ...', _events); 71 | for (const _evnt of _events) { 72 | const text = `Please approve my NIP-05 request on https://nip05.nostprotocol.net @${Deno.env.get('NPUBLIC_KEY')}`; 73 | console.log('Content checking...', _evnt.content); 74 | if (_evnt.content === text) { 75 | await this.verifyUser(_evnt.pubkey, usr); 76 | console.log(`${usr.name} request is approved.`); 77 | } 78 | } 79 | } 80 | console.log('Receiving messages is finished.'); 81 | this.intervalId = setInterval(this.checkUsers.bind(this), 1000 * 60 * 15); 82 | } catch (err) { 83 | console.error('checkUsers error;', err); 84 | } 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /src/server/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | dotenv@^16.0.3: 6 | version "16.0.3" 7 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" 8 | integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== 9 | --------------------------------------------------------------------------------