mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-13 03:53:22 +00:00
Compare commits
45 Commits
158e001055
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 77d657e59f | |||
| b89e16bc0d | |||
| 10673c7b62 | |||
| 7af4362b53 | |||
| 056c027083 | |||
| 08bee1a6f7 | |||
| 9b5b35ef7d | |||
| b9c07aeb5a | |||
| 7747f59060 | |||
| d7caa81042 | |||
| ac4826e92f | |||
| 1e46cef35f | |||
| 9e0728f74a | |||
| 778fa2e3a9 | |||
| 8bb951a91c | |||
| 833bf7e519 | |||
| 508f050f6e | |||
| cf13672d85 | |||
| 37eabff69c | |||
| 758fb2d4ec | |||
| 4d95ddcd59 | |||
| 023be0ab67 | |||
| 8959dd6616 | |||
| 5345ebb72f | |||
| d6b441e352 | |||
| 5789fc636b | |||
| 830f411291 | |||
| 0640afc32c | |||
| a0c5ae18b4 | |||
| 00c1c21e6c | |||
| cd93226ea1 | |||
| 2d7a88ba5f | |||
| 55e197a5c5 | |||
| a08dd1dccc | |||
| 249d1366d3 | |||
| 91b845aa1c | |||
| db651d4193 | |||
| a6329c8708 | |||
| 538315c51f | |||
| 6a24514e7b | |||
| 46f0eb2e7a | |||
| 9e26ed3b66 | |||
| 7835ebd35d | |||
| 5f223356ba | |||
| ab55a96fac |
@@ -296,6 +296,15 @@ await peer.createOffer({
|
|||||||
- **RTCDataChannel** - P2P messaging
|
- **RTCDataChannel** - P2P messaging
|
||||||
- **QRCode** - QR code generation for easy topic sharing
|
- **QRCode** - QR code generation for easy topic sharing
|
||||||
|
|
||||||
|
## Node.js Service Hosting
|
||||||
|
|
||||||
|
Want to create a Node.js service that browser clients can connect to? See:
|
||||||
|
- **[NODE_HOST_GUIDE.md](NODE_HOST_GUIDE.md)** - Complete guide to hosting WebRTC services in Node.js
|
||||||
|
- **[test-connect.js](test-connect.js)** - Working example of a Node.js client
|
||||||
|
- **[TEST_README.md](TEST_README.md)** - Instructions for running the test client
|
||||||
|
|
||||||
|
Perfect for creating chat bots, data processors, game servers, or any service that browsers can connect to via WebRTC!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
67
TEST_README.md
Normal file
67
TEST_README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Running Node.js Tests
|
||||||
|
|
||||||
|
The `test-connect.js` script demonstrates connecting to a Rondevu service from Node.js and sending a WebRTC data channel message.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js 19+ (or Node.js 18 with `--experimental-global-webcrypto` flag)
|
||||||
|
- wrtc package (for WebRTC support in Node.js)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The `wrtc` package requires native compilation. Due to build complexities, it's not included as a regular dependency.
|
||||||
|
|
||||||
|
### Install wrtc manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install build tools (if not already installed)
|
||||||
|
# On Ubuntu/Debian:
|
||||||
|
sudo apt-get install build-essential python3
|
||||||
|
|
||||||
|
# On macOS:
|
||||||
|
xcode-select --install
|
||||||
|
|
||||||
|
# Install wrtc
|
||||||
|
npm install wrtc
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Installation may take several minutes as it compiles native code.
|
||||||
|
|
||||||
|
### Alternative: Test without WebRTC
|
||||||
|
|
||||||
|
If wrtc installation fails, you can still test the signaling layer without actual WebRTC connections by modifying the test script or using the browser demo at https://ronde.vu
|
||||||
|
|
||||||
|
## Running the Test
|
||||||
|
|
||||||
|
Once wrtc is installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Connect to the production Rondevu server
|
||||||
|
2. Look for @bas's chat service
|
||||||
|
3. Establish a WebRTC connection
|
||||||
|
4. Send "hello" via data channel
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### wrtc installation fails
|
||||||
|
|
||||||
|
Try installing dependencies:
|
||||||
|
```bash
|
||||||
|
npm install node-pre-gyp node-gyp
|
||||||
|
npm install wrtc
|
||||||
|
```
|
||||||
|
|
||||||
|
### "crypto.subtle is not available"
|
||||||
|
|
||||||
|
You need Node.js 19+ or run with:
|
||||||
|
```bash
|
||||||
|
node --experimental-global-webcrypto test-connect.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't find @bas's service
|
||||||
|
|
||||||
|
The test looks for `chat:1.0.0@bas`. If @bas is not online or the service expired, the test will fail. You can modify the `TARGET_USER` constant to test with a different user.
|
||||||
99
package-lock.json
generated
99
package-lock.json
generated
@@ -8,12 +8,13 @@
|
|||||||
"name": "rondevu-demo",
|
"name": "rondevu-demo",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xtr-dev/rondevu-client": "^0.12.0",
|
"@xtr-dev/rondevu-client": "file:../client",
|
||||||
"@zxing/library": "^0.21.3",
|
"@zxing/library": "^0.21.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.6.0"
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"wrtc": "^0.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
@@ -22,6 +23,27 @@
|
|||||||
"vite": "^5.4.11"
|
"vite": "^5.4.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../client": {
|
||||||
|
"name": "@xtr-dev/rondevu-client",
|
||||||
|
"version": "0.16.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/ed25519": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||||
|
"@typescript-eslint/parser": "^8.48.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.4",
|
||||||
|
"eslint-plugin-unicorn": "^62.0.0",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"prettier": "^3.7.4",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.2.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -745,15 +767,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@noble/ed25519": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
@@ -1171,13 +1184,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@xtr-dev/rondevu-client": {
|
"node_modules/@xtr-dev/rondevu-client": {
|
||||||
"version": "0.12.0",
|
"resolved": "../client",
|
||||||
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.12.0.tgz",
|
"link": true
|
||||||
"integrity": "sha512-c1UecF29Cjck7h7b7KWyCti8YSVVkuvEzyAz7aaFwAYBEumgX1r143mTBRfAMQkFL4upkG/PL5bvBGRY9QCpug==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@noble/ed25519": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@zxing/library": {
|
"node_modules/@zxing/library": {
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
@@ -1226,9 +1234,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.3",
|
"version": "2.9.7",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz",
|
||||||
"integrity": "sha512-8QdH6czo+G7uBsNo0GiUfouPN1lRzKdJTGnKXwe12gkFbnnOUaUKGN55dMkfy+mnxmvjwl9zcI4VncczcVXDhA==",
|
"integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1279,9 +1287,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001759",
|
"version": "1.0.30001760",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
|
||||||
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
|
"integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1374,10 +1382,21 @@
|
|||||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/domexception": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
|
||||||
|
"deprecated": "Use your platform's native DOMException instead",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"webidl-conversions": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.266",
|
"version": "1.5.267",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||||
"integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==",
|
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -1977,6 +1996,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/which-module": {
|
"node_modules/which-module": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
@@ -1997,6 +2023,25 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wrtc": {
|
||||||
|
"version": "0.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrtc/-/wrtc-0.4.7.tgz",
|
||||||
|
"integrity": "sha512-P6Hn7VT4lfSH49HxLHcHhDq+aFf/jd9dPY7lDHeFhZ22N3858EKuwm2jmnlPzpsRGEPaoF6XwkcxY5SYnt4f/g==",
|
||||||
|
"bundleDependencies": [
|
||||||
|
"node-pre-gyp"
|
||||||
|
],
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"node-pre-gyp": "^0.13.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.11.2 || >=10.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"domexception": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
|||||||
@@ -7,15 +7,17 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo"
|
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo",
|
||||||
|
"test": "node test-connect.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xtr-dev/rondevu-client": "^0.12.0",
|
"@xtr-dev/rondevu-client": "file:../client",
|
||||||
"@zxing/library": "^0.21.3",
|
"@zxing/library": "^0.21.3",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.6.0"
|
"react-hot-toast": "^2.6.0",
|
||||||
|
"wrtc": "^0.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
|
|||||||
1368
src/App-old.jsx
1368
src/App-old.jsx
File diff suppressed because it is too large
Load Diff
1605
src/App.jsx
1605
src/App.jsx
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
|||||||
import QRScanner from './QRScanner';
|
|
||||||
|
|
||||||
function ActionSelector({ action, onSelectAction, onScanComplete, onScanCancel, log }) {
|
|
||||||
return (
|
|
||||||
<div className="step-container">
|
|
||||||
<h2>Chat Demo</h2>
|
|
||||||
<div className="button-grid button-grid-three">
|
|
||||||
<button
|
|
||||||
className="action-button"
|
|
||||||
onClick={() => onSelectAction('create')}
|
|
||||||
>
|
|
||||||
<div className="button-title">Create</div>
|
|
||||||
<div className="button-description">Start a new connection</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="action-button"
|
|
||||||
onClick={() => onSelectAction('join')}
|
|
||||||
>
|
|
||||||
<div className="button-title">Join</div>
|
|
||||||
<div className="button-description">Connect to existing peers</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="action-button"
|
|
||||||
onClick={() => onSelectAction('scan')}
|
|
||||||
>
|
|
||||||
<div className="button-title">Scan QR</div>
|
|
||||||
<div className="button-description">Scan a connection code</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{action === 'scan' && (
|
|
||||||
<QRScanner onScan={onScanComplete} onCancel={onScanCancel} log={log} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ActionSelector;
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { useRef } from 'react';
|
|
||||||
import Message from './Message';
|
|
||||||
import FileUploadProgress from './FileUploadProgress';
|
|
||||||
|
|
||||||
function ChatView({
|
|
||||||
connectedPeer,
|
|
||||||
currentConnectionId,
|
|
||||||
messages,
|
|
||||||
messageInput,
|
|
||||||
setMessageInput,
|
|
||||||
channelReady,
|
|
||||||
logs,
|
|
||||||
fileUploadProgress,
|
|
||||||
onSendMessage,
|
|
||||||
onFileSelect,
|
|
||||||
onDisconnect,
|
|
||||||
onDownloadFile,
|
|
||||||
onCancelUpload
|
|
||||||
}) {
|
|
||||||
const fileInputRef = useRef(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="chat-container">
|
|
||||||
<div className="chat-header">
|
|
||||||
<div>
|
|
||||||
<h2>Connected</h2>
|
|
||||||
<p className="connection-details">
|
|
||||||
Peer: {connectedPeer || 'Unknown'} • ID: {currentConnectionId}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button className="disconnect-button" onClick={onDisconnect}>Disconnect</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="messages">
|
|
||||||
{messages.length === 0 ? (
|
|
||||||
<p className="empty">No messages yet. Start chatting!</p>
|
|
||||||
) : (
|
|
||||||
messages.map((msg, idx) => (
|
|
||||||
<Message key={idx} message={msg} onDownload={onDownloadFile} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{fileUploadProgress && (
|
|
||||||
<FileUploadProgress
|
|
||||||
fileName={fileUploadProgress.fileName}
|
|
||||||
progress={fileUploadProgress.progress}
|
|
||||||
onCancel={onCancelUpload}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="message-input">
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
onChange={onFileSelect}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="file-button"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
disabled={!channelReady || fileUploadProgress}
|
|
||||||
title="Send file"
|
|
||||||
>
|
|
||||||
📎
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={messageInput}
|
|
||||||
onChange={(e) => setMessageInput(e.target.value)}
|
|
||||||
onKeyPress={(e) => e.key === 'Enter' && onSendMessage()}
|
|
||||||
placeholder="Type a message..."
|
|
||||||
disabled={!channelReady}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={onSendMessage}
|
|
||||||
disabled={!channelReady}
|
|
||||||
>
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{logs.length > 0 && (
|
|
||||||
<details className="logs">
|
|
||||||
<summary>Activity Log ({logs.length})</summary>
|
|
||||||
<div className="log-entries">
|
|
||||||
{logs.map((log, idx) => (
|
|
||||||
<div key={idx} className={`log-entry ${log.type}`}>
|
|
||||||
[{log.timestamp}] {log.message}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ChatView;
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import QRCodeDisplay from './QRCodeDisplay';
|
|
||||||
|
|
||||||
function ConnectionForm({
|
|
||||||
action,
|
|
||||||
connectionId,
|
|
||||||
setConnectionId,
|
|
||||||
connectionStatus,
|
|
||||||
qrCodeUrl,
|
|
||||||
currentConnectionId,
|
|
||||||
onConnect,
|
|
||||||
onBack
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="step-container">
|
|
||||||
<h2>{action === 'create' ? 'Create Connection' : 'Join Connection'}</h2>
|
|
||||||
<div className="form-container">
|
|
||||||
<div className="form-group">
|
|
||||||
<label>Connection ID {action === 'create' && '(optional)'}</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={connectionId}
|
|
||||||
onChange={(e) => setConnectionId(e.target.value)}
|
|
||||||
placeholder={action === 'create' ? 'Auto-generated if empty' : 'Enter connection ID'}
|
|
||||||
autoFocus={action === 'connect'}
|
|
||||||
/>
|
|
||||||
{action === 'create' && !connectionId && (
|
|
||||||
<p className="help-text">Leave empty to auto-generate a random ID</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="button-row">
|
|
||||||
<button className="back-button" onClick={onBack}>← Back</button>
|
|
||||||
<button
|
|
||||||
className="primary-button"
|
|
||||||
onClick={onConnect}
|
|
||||||
disabled={
|
|
||||||
connectionStatus === 'connecting' ||
|
|
||||||
(action === 'connect' && !connectionId)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{connectionStatus === 'connecting' ? 'Connecting...' : (action === 'create' ? 'Create' : 'Connect')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{qrCodeUrl && connectionStatus === 'connecting' && action === 'create' && (
|
|
||||||
<QRCodeDisplay qrCodeUrl={qrCodeUrl} connectionId={currentConnectionId} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ConnectionForm;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
function FileUploadProgress({ fileName, progress, onCancel }) {
|
|
||||||
return (
|
|
||||||
<div className="file-upload-progress">
|
|
||||||
<div className="file-upload-header">
|
|
||||||
<span className="file-upload-name">{fileName}</span>
|
|
||||||
<button className="file-upload-cancel" onClick={onCancel}>×</button>
|
|
||||||
</div>
|
|
||||||
<div className="progress-bar">
|
|
||||||
<div className="progress-bar-fill" style={{ width: `${progress}%` }}>
|
|
||||||
<span className="progress-text">{progress}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileUploadProgress;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
function Header() {
|
|
||||||
return (
|
|
||||||
<header className="header">
|
|
||||||
<div className="header-content">
|
|
||||||
<h1>Rondevu</h1>
|
|
||||||
<p className="tagline">Simple WebRTC peer signaling and discovery. Meet peers by topic, peer ID, or connection ID.</p>
|
|
||||||
<div className="header-links">
|
|
||||||
<a href="https://github.com/xtr-dev/rondevu-client" target="_blank" rel="noopener noreferrer">
|
|
||||||
<svg className="github-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
|
||||||
</svg>
|
|
||||||
Client
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/xtr-dev/rondevu-server" target="_blank" rel="noopener noreferrer">
|
|
||||||
<svg className="github-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
|
||||||
</svg>
|
|
||||||
Server
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/xtr-dev/rondevu-demo" target="_blank" rel="noopener noreferrer">
|
|
||||||
<svg className="github-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
|
||||||
</svg>
|
|
||||||
Demo
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
function Message({ message, onDownload }) {
|
|
||||||
const isFile = message.messageType === 'file';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`message ${message.type}`}>
|
|
||||||
{isFile ? (
|
|
||||||
<div className="message-file">
|
|
||||||
<div className="file-icon">📎</div>
|
|
||||||
<div className="file-info">
|
|
||||||
<div className="file-name">{message.file.name}</div>
|
|
||||||
<div className="file-size">{(message.file.size / 1024).toFixed(2)} KB</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="file-download"
|
|
||||||
onClick={() => onDownload(message.file)}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="message-text">{message.text}</div>
|
|
||||||
)}
|
|
||||||
<div className="message-time">{message.timestamp.toLocaleTimeString()}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Message;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
function MethodSelector({ action, onSelectMethod, onBack }) {
|
|
||||||
return (
|
|
||||||
<div className="step-container">
|
|
||||||
<h2>{action === 'create' ? 'Create' : 'Join'} by...</h2>
|
|
||||||
<div className="button-grid">
|
|
||||||
<button
|
|
||||||
className="action-button"
|
|
||||||
onClick={() => onSelectMethod('topic')}
|
|
||||||
>
|
|
||||||
<div className="button-title">Topic</div>
|
|
||||||
<div className="button-description">
|
|
||||||
{action === 'create' ? 'Create in a topic' : 'Auto-connect to first peer'}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{action === 'join' && (
|
|
||||||
<button
|
|
||||||
className="action-button"
|
|
||||||
onClick={() => onSelectMethod('peer-id')}
|
|
||||||
>
|
|
||||||
<div className="button-title">Peer ID</div>
|
|
||||||
<div className="button-description">Connect to specific peer</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className="action-button"
|
|
||||||
onClick={() => onSelectMethod('connection-id')}
|
|
||||||
>
|
|
||||||
<div className="button-title">Connection ID</div>
|
|
||||||
<div className="button-description">
|
|
||||||
{action === 'create' ? 'Custom connection code' : 'Direct connection'}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button className="back-button" onClick={onBack}>← Back</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MethodSelector;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
function QRCodeDisplay({ qrCodeUrl, connectionId }) {
|
|
||||||
if (!qrCodeUrl) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="qr-code-container">
|
|
||||||
<p className="qr-label">Scan to connect:</p>
|
|
||||||
<img src={qrCodeUrl} alt="Connection QR Code" className="qr-code" />
|
|
||||||
<p className="connection-id-display">{connectionId}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QRCodeDisplay;
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { useRef, useEffect } from 'react';
|
|
||||||
import { BrowserQRCodeReader } from '@zxing/library';
|
|
||||||
|
|
||||||
function QRScanner({ onScan, onCancel, log }) {
|
|
||||||
const videoRef = useRef(null);
|
|
||||||
const scannerRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
startScanning();
|
|
||||||
return () => {
|
|
||||||
stopScanning();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const startScanning = async () => {
|
|
||||||
try {
|
|
||||||
scannerRef.current = new BrowserQRCodeReader();
|
|
||||||
log('Starting QR scanner...', 'info');
|
|
||||||
|
|
||||||
const videoInputDevices = await scannerRef.current.listVideoInputDevices();
|
|
||||||
|
|
||||||
if (videoInputDevices.length === 0) {
|
|
||||||
log('No camera found', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer back camera (environment-facing)
|
|
||||||
let selectedDeviceId = videoInputDevices[0].deviceId;
|
|
||||||
const backCamera = videoInputDevices.find(device =>
|
|
||||||
device.label.toLowerCase().includes('back') ||
|
|
||||||
device.label.toLowerCase().includes('rear') ||
|
|
||||||
device.label.toLowerCase().includes('environment')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (backCamera) {
|
|
||||||
selectedDeviceId = backCamera.deviceId;
|
|
||||||
log('Using back camera', 'info');
|
|
||||||
} else {
|
|
||||||
log('Back camera not found, using default', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
scannerRef.current.decodeFromVideoDevice(
|
|
||||||
selectedDeviceId,
|
|
||||||
videoRef.current,
|
|
||||||
(result, err) => {
|
|
||||||
if (result) {
|
|
||||||
const scannedId = result.getText();
|
|
||||||
log(`Scanned: ${scannedId}`, 'success');
|
|
||||||
stopScanning();
|
|
||||||
onScan(scannedId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
log(`Scanner error: ${error.message}`, 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopScanning = () => {
|
|
||||||
if (scannerRef.current) {
|
|
||||||
scannerRef.current.reset();
|
|
||||||
log('Scanner stopped', 'info');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="scanner-container">
|
|
||||||
<video ref={videoRef} className="scanner-video" />
|
|
||||||
<button className="back-button" onClick={onCancel}>← Cancel</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QRScanner;
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
function TopicsList({ rdv, onClose }) {
|
|
||||||
const [topics, setTopics] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [pagination, setPagination] = useState(null);
|
|
||||||
const [limit] = useState(20);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadTopics();
|
|
||||||
}, [page]);
|
|
||||||
|
|
||||||
const loadTopics = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const response = await rdv.api.listTopics(page, limit);
|
|
||||||
setTopics(response.topics);
|
|
||||||
setPagination(response.pagination);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
loadTopics();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrevPage = () => {
|
|
||||||
if (page > 1) {
|
|
||||||
setPage(page - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextPage = () => {
|
|
||||||
if (pagination?.hasMore) {
|
|
||||||
setPage(page + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="modal-overlay" onClick={onClose}>
|
|
||||||
<div className="modal-content topics-modal" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="modal-header">
|
|
||||||
<h2>Active Topics</h2>
|
|
||||||
<button className="close-button" onClick={onClose}>×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-body">
|
|
||||||
{error && (
|
|
||||||
<div className="error-message" style={{ marginBottom: '1rem' }}>
|
|
||||||
Error: {error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="loading-message">Loading topics...</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{topics.length === 0 ? (
|
|
||||||
<div className="empty-message">
|
|
||||||
No active topics found. Be the first to create one!
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="topics-list">
|
|
||||||
{topics.map((topic) => (
|
|
||||||
<div key={topic.topic} className="topic-item">
|
|
||||||
<div className="topic-name">{topic.topic}</div>
|
|
||||||
<div className="topic-count">
|
|
||||||
{topic.count} {topic.count === 1 ? 'peer' : 'peers'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{pagination && (
|
|
||||||
<div className="pagination">
|
|
||||||
<button
|
|
||||||
onClick={handlePrevPage}
|
|
||||||
disabled={page === 1}
|
|
||||||
className="pagination-button"
|
|
||||||
>
|
|
||||||
← Previous
|
|
||||||
</button>
|
|
||||||
<span className="pagination-info">
|
|
||||||
Page {pagination.page} of {Math.ceil(pagination.total / pagination.limit)}
|
|
||||||
{' '}({pagination.total} total)
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={handleNextPage}
|
|
||||||
disabled={!pagination.hasMore}
|
|
||||||
className="pagination-button"
|
|
||||||
>
|
|
||||||
Next →
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button onClick={handleRefresh} className="button button-secondary">
|
|
||||||
🔄 Refresh
|
|
||||||
</button>
|
|
||||||
<button onClick={onClose} className="button button-primary">
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TopicsList;
|
|
||||||
146
test-connect.js
Normal file
146
test-connect.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Test script to connect to @bas's chat service and send a "hello" message
|
||||||
|
*
|
||||||
|
* IMPORTANT: This script requires the 'wrtc' package which must be installed separately.
|
||||||
|
* See TEST_README.md for detailed installation instructions.
|
||||||
|
*
|
||||||
|
* Quick start:
|
||||||
|
* npm install wrtc
|
||||||
|
* npm test
|
||||||
|
*
|
||||||
|
* Requirements:
|
||||||
|
* - Node.js 19+ (or Node.js 18 with --experimental-global-webcrypto)
|
||||||
|
* - wrtc package (requires native compilation)
|
||||||
|
* - Build tools (python, make, g++)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
|
||||||
|
|
||||||
|
// Import wrtc
|
||||||
|
let wrtc
|
||||||
|
try {
|
||||||
|
const wrtcModule = await import('wrtc')
|
||||||
|
wrtc = wrtcModule.default || wrtcModule
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error: wrtc package not found or failed to load')
|
||||||
|
console.error('\nThe wrtc package is required for WebRTC support in Node.js.')
|
||||||
|
console.error('Install it with:')
|
||||||
|
console.error('\n npm install wrtc')
|
||||||
|
console.error('\nNote: wrtc requires native compilation and may take a few minutes to install.')
|
||||||
|
console.error('\nError details:', error.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { RTCPeerConnection } = wrtc
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const API_URL = 'https://api.ronde.vu'
|
||||||
|
const TARGET_USER = 'bas'
|
||||||
|
const SERVICE_FQN = `chat:2.0.0@${TARGET_USER}`
|
||||||
|
const MESSAGE = 'hello'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🚀 Rondevu Test Script')
|
||||||
|
console.log('='.repeat(50))
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Connect to Rondevu with Node crypto adapter and ICE preset
|
||||||
|
console.log('1. Connecting to Rondevu...')
|
||||||
|
const rondevu = await Rondevu.connect({
|
||||||
|
apiUrl: API_URL,
|
||||||
|
username: `test-${Date.now()}`, // Anonymous test user
|
||||||
|
cryptoAdapter: new NodeCryptoAdapter(),
|
||||||
|
iceServers: 'ipv4-turn' // Use ICE server preset
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(` ✓ Connected as: ${rondevu.getUsername()}`)
|
||||||
|
console.log(` ✓ Public key: ${rondevu.getPublicKey()?.substring(0, 20)}...`)
|
||||||
|
|
||||||
|
// 2. Connect to service (automatic setup)
|
||||||
|
console.log(`\n2. Connecting to service: ${SERVICE_FQN}`)
|
||||||
|
let identified = false
|
||||||
|
|
||||||
|
const connection = await rondevu.connectToService({
|
||||||
|
serviceFqn: SERVICE_FQN,
|
||||||
|
onConnection: ({ dc, peerUsername }) => {
|
||||||
|
console.log(`✅ Connected to @${peerUsername}`)
|
||||||
|
|
||||||
|
// Set up message handler
|
||||||
|
dc.addEventListener('message', (event) => {
|
||||||
|
console.log(`📥 RAW DATA:`, event.data)
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(event.data)
|
||||||
|
console.log(`📥 Parsed message:`, JSON.stringify(msg, null, 2))
|
||||||
|
|
||||||
|
if (msg.type === 'identify_ack' && !identified) {
|
||||||
|
identified = true
|
||||||
|
console.log(`✅ Connection acknowledged by @${msg.from}`)
|
||||||
|
|
||||||
|
// Now send the actual chat message
|
||||||
|
console.log(`📤 Sending chat message: "${MESSAGE}"`)
|
||||||
|
dc.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
text: MESSAGE
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Keep connection open longer to see if we get a response
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('\n✅ Test completed successfully!')
|
||||||
|
connection.dc.close()
|
||||||
|
connection.pc.close()
|
||||||
|
process.exit(0)
|
||||||
|
}, 5000)
|
||||||
|
} else if (msg.type === 'message') {
|
||||||
|
console.log(`💬 @${msg.from || 'peer'}: ${msg.text}`)
|
||||||
|
} else {
|
||||||
|
console.log(`📥 Unknown message type: ${msg.type}`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`📥 Parse error:`, err.message)
|
||||||
|
console.log(`📥 Raw data was:`, event.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send identify message after channel opens
|
||||||
|
console.log(`📤 Sending identify message...`)
|
||||||
|
const identifyMsg = JSON.stringify({
|
||||||
|
type: 'identify',
|
||||||
|
from: rondevu.getUsername()
|
||||||
|
})
|
||||||
|
dc.send(identifyMsg)
|
||||||
|
console.log(` ✓ Identify message sent`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Monitor connection state
|
||||||
|
connection.pc.onconnectionstatechange = () => {
|
||||||
|
console.log(` Connection state: ${connection.pc.connectionState}`)
|
||||||
|
if (connection.pc.connectionState === 'failed') {
|
||||||
|
console.error('❌ Connection failed')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.pc.oniceconnectionstatechange = () => {
|
||||||
|
console.log(` ICE state: ${connection.pc.iceConnectionState}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n⏳ Waiting for messages...')
|
||||||
|
|
||||||
|
// Timeout after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (connection.pc.connectionState !== 'connected') {
|
||||||
|
console.error('❌ Connection timeout')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Error:', error.message)
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user