0x0-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/0x0.js +141 -0
- package/dist/web/assets/index-BrxSM83h.css +1 -0
- package/dist/web/assets/index-Ci89CtgQ.js +176 -0
- package/dist/web/index.html +15 -0
- package/package.json +37 -0
- package/src/commands/chat.js +204 -0
- package/src/commands/contact.js +63 -0
- package/src/commands/inbox.js +68 -0
- package/src/commands/init.js +31 -0
- package/src/commands/listen.js +74 -0
- package/src/commands/pin.js +114 -0
- package/src/commands/pipe.js +121 -0
- package/src/commands/qr.js +35 -0
- package/src/commands/read.js +48 -0
- package/src/commands/renew.js +28 -0
- package/src/commands/send.js +61 -0
- package/src/commands/web.js +37 -0
- package/src/commands/whoami.js +24 -0
- package/src/core/channel.js +10 -0
- package/src/core/identity.js +8 -0
- package/src/core/message.js +35 -0
- package/src/core/pin.js +10 -0
- package/src/core/swarm.js +12 -0
- package/src/core/uri.js +12 -0
- package/src/storage/contacts.js +72 -0
- package/src/storage/identity.js +30 -0
- package/src/storage/messages.js +48 -0
- package/src/storage/pins.js +71 -0
- package/src/web/api.js +375 -0
- package/src/web/server.js +52 -0
package/bin/0x0.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander'
|
|
3
|
+
import { cmdInit } from '../src/commands/init.js'
|
|
4
|
+
import { cmdWhoami } from '../src/commands/whoami.js'
|
|
5
|
+
import { cmdRenew } from '../src/commands/renew.js'
|
|
6
|
+
import {
|
|
7
|
+
cmdPinNew, cmdPinList, cmdPinRotate, cmdPinRevoke, cmdPinInfo
|
|
8
|
+
} from '../src/commands/pin.js'
|
|
9
|
+
import { cmdSend } from '../src/commands/send.js'
|
|
10
|
+
import { cmdInbox } from '../src/commands/inbox.js'
|
|
11
|
+
import { cmdRead } from '../src/commands/read.js'
|
|
12
|
+
import { cmdChat } from '../src/commands/chat.js'
|
|
13
|
+
import { cmdWeb } from '../src/commands/web.js'
|
|
14
|
+
import { cmdListen } from '../src/commands/listen.js'
|
|
15
|
+
import { cmdPipe } from '../src/commands/pipe.js'
|
|
16
|
+
import {
|
|
17
|
+
cmdContactAdd, cmdContactList, cmdContactLabel, cmdContactRemove
|
|
18
|
+
} from '../src/commands/contact.js'
|
|
19
|
+
import { cmdQr } from '../src/commands/qr.js'
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name('0x0')
|
|
23
|
+
.description('P2P disposable number messenger')
|
|
24
|
+
.version('1.0.0')
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('init')
|
|
28
|
+
.description('initialize 0x0 and generate your number')
|
|
29
|
+
.action(cmdInit)
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('whoami')
|
|
33
|
+
.description('show your number and active pins')
|
|
34
|
+
.action(cmdWhoami)
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('renew')
|
|
38
|
+
.description('renew your number (resets all PINs)')
|
|
39
|
+
.action(cmdRenew)
|
|
40
|
+
|
|
41
|
+
// pin subcommands
|
|
42
|
+
const pin = program.command('pin').description('manage PINs')
|
|
43
|
+
|
|
44
|
+
pin
|
|
45
|
+
.command('new')
|
|
46
|
+
.description('create a new PIN')
|
|
47
|
+
.option('-l, --label <label>', 'label for this PIN')
|
|
48
|
+
.option('-e, --expires <duration>', 'expiry duration (e.g. 24h, 7d, 1w)')
|
|
49
|
+
.option('--once', 'expire after receiving one message')
|
|
50
|
+
.action((opts) => cmdPinNew(opts))
|
|
51
|
+
|
|
52
|
+
pin
|
|
53
|
+
.command('list')
|
|
54
|
+
.description('list all active PINs')
|
|
55
|
+
.action(cmdPinList)
|
|
56
|
+
|
|
57
|
+
pin
|
|
58
|
+
.command('rotate <pin>')
|
|
59
|
+
.description('rotate a PIN (generates new value)')
|
|
60
|
+
.action(cmdPinRotate)
|
|
61
|
+
|
|
62
|
+
pin
|
|
63
|
+
.command('revoke <pin>')
|
|
64
|
+
.description('revoke a PIN immediately')
|
|
65
|
+
.action(cmdPinRevoke)
|
|
66
|
+
|
|
67
|
+
pin
|
|
68
|
+
.command('info <pin>')
|
|
69
|
+
.description('show PIN details')
|
|
70
|
+
.action(cmdPinInfo)
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command('chat <number> <pin>')
|
|
74
|
+
.description('start interactive P2P chat')
|
|
75
|
+
.action(cmdChat)
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.command('send <number> <pin> <message>')
|
|
79
|
+
.description('send a single message and exit')
|
|
80
|
+
.action(cmdSend)
|
|
81
|
+
|
|
82
|
+
program
|
|
83
|
+
.command('inbox')
|
|
84
|
+
.description('show inbox (all pins)')
|
|
85
|
+
.option('--json', 'output as JSON')
|
|
86
|
+
.action((opts) => cmdInbox(opts))
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.command('read <pin>')
|
|
90
|
+
.description('read messages for a specific PIN')
|
|
91
|
+
.option('--json', 'output as JSON')
|
|
92
|
+
.action((p, opts) => cmdRead(p, opts))
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command('listen')
|
|
96
|
+
.description('listen for incoming messages (daemon mode)')
|
|
97
|
+
.option('-p, --pin <pin>', 'listen on a specific PIN only')
|
|
98
|
+
.action((opts) => cmdListen(opts))
|
|
99
|
+
|
|
100
|
+
program
|
|
101
|
+
.command('pipe <number> <pin>')
|
|
102
|
+
.description('stdio mode for agents (JSON stream)')
|
|
103
|
+
.action(cmdPipe)
|
|
104
|
+
|
|
105
|
+
program
|
|
106
|
+
.command('web')
|
|
107
|
+
.description('start browser UI (localhost)')
|
|
108
|
+
.option('-p, --port <port>', 'port number', '3000')
|
|
109
|
+
.option('--no-open', 'do not open browser automatically')
|
|
110
|
+
.action((opts) => cmdWeb({ port: parseInt(opts.port), noOpen: !opts.open }))
|
|
111
|
+
|
|
112
|
+
// contact subcommands
|
|
113
|
+
const contact = program.command('contact').description('manage contacts')
|
|
114
|
+
|
|
115
|
+
contact
|
|
116
|
+
.command('add <uri-or-number> [pin]')
|
|
117
|
+
.description('add a contact (URI or number+pin)')
|
|
118
|
+
.option('-l, --label <label>', 'label for this contact')
|
|
119
|
+
.action((uriOrNumber, pin, opts) => cmdContactAdd(uriOrNumber, pin, opts))
|
|
120
|
+
|
|
121
|
+
contact
|
|
122
|
+
.command('list')
|
|
123
|
+
.description('list all contacts')
|
|
124
|
+
.action(cmdContactList)
|
|
125
|
+
|
|
126
|
+
contact
|
|
127
|
+
.command('label <id> <label>')
|
|
128
|
+
.description('update contact label')
|
|
129
|
+
.action(cmdContactLabel)
|
|
130
|
+
|
|
131
|
+
contact
|
|
132
|
+
.command('remove <id>')
|
|
133
|
+
.description('remove a contact')
|
|
134
|
+
.action(cmdContactRemove)
|
|
135
|
+
|
|
136
|
+
program
|
|
137
|
+
.command('qr <pin>')
|
|
138
|
+
.description('show QR code for a PIN (share to let others connect)')
|
|
139
|
+
.action(cmdQr)
|
|
140
|
+
|
|
141
|
+
program.parse()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg: #000000;--surface: #111111;--border: #222222;--border-strong:#444444;--text-muted: #888888;--text-sec: #aaaaaa;--text: #ffffff;--page-bg: #0a0a0a;--mono: "Share Tech Mono", monospace;--sans: "Noto Sans JP", sans-serif}:root.light{--bg: #ffffff;--surface: #f5f5f5;--border: #e0e0e0;--border-strong:#bbbbbb;--text-muted: #666666;--text-sec: #444444;--text: #111111;--page-bg: #eeeeee}*,*:before,*:after{margin:0;padding:0;box-sizing:border-box}html,body{height:100%}body{background:var(--page-bg);color:var(--text);font-family:var(--mono);font-size:13px;cursor:crosshair;overflow:hidden;transition:background .3s,color .3s}body:before{content:"";position:fixed;top:0;right:0;bottom:0;left:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.03) 2px,rgba(0,0,0,.03) 4px);pointer-events:none;z-index:9999}button{cursor:crosshair;font-family:var(--mono)}input,textarea{font-family:var(--sans)}#app{height:100vh;display:flex;flex-direction:column}.topbar{display:flex;align-items:center;justify-content:space-between;padding:0 20px;height:40px;border-bottom:1px solid var(--border);flex-shrink:0;background:var(--bg)}.topbar-logo{font-size:14px;color:var(--text);letter-spacing:.1em}.theme-toggle{display:flex;align-items:center;gap:8px;font-size:10px;color:var(--text-muted);letter-spacing:.2em;background:none;border:none;padding:4px 8px;transition:color .2s}.theme-toggle:hover{color:var(--text)}.main-area{flex:1;display:flex;overflow:hidden}.sidebar{width:280px;flex-shrink:0;border-right:1px solid var(--border);display:flex;flex-direction:column;background:var(--bg);transition:background .3s,border-color .3s}.number-section{padding:16px 20px 12px;border-bottom:1px solid var(--border)}.section-label{font-size:10px;color:var(--border-strong);letter-spacing:.3em;margin-bottom:8px;display:flex;align-items:center;gap:8px}.section-label:after{content:"";flex:1;height:1px;background:var(--border)}.my-number{font-size:12px;color:var(--text);letter-spacing:.05em;margin-bottom:8px;word-break:break-all}.my-number.glitching{color:var(--text-muted)}.number-actions{display:flex;gap:6px;flex-wrap:wrap}.pill-btn{font-size:10px;color:var(--text-muted);border:1px solid var(--border-strong);background:transparent;padding:3px 8px;letter-spacing:.1em;transition:all .2s}.pill-btn:hover{background:var(--border-strong);color:var(--bg)}.inbox-section{flex:1;overflow-y:auto;padding:4px 0}.inbox-item{display:flex;align-items:center;padding:12px 20px;border-bottom:1px solid var(--border);cursor:crosshair;gap:12px;transition:background .15s;position:relative}.inbox-item:hover,.inbox-item.active{background:var(--surface)}.inbox-pin{font-size:11px;color:var(--text-sec);width:34px;flex-shrink:0}.inbox-info{flex:1;min-width:0}.inbox-label{font-family:var(--sans);font-size:12px;color:var(--text);font-weight:400;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.inbox-preview{font-family:var(--sans);font-size:11px;color:var(--text-muted);font-weight:300;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.inbox-meta{text-align:right;flex-shrink:0}.inbox-time{font-size:10px;color:var(--border-strong);margin-bottom:4px}.inbox-badge{width:16px;height:16px;border-radius:50%;background:var(--text);color:var(--bg);font-size:9px;display:flex;align-items:center;justify-content:center;margin-left:auto;transition:background .3s,color .3s}.inbox-badge[data-count="0"]{visibility:hidden}.peer-dot{width:6px;height:6px;border-radius:50%;background:var(--border-strong);flex-shrink:0;transition:background .3s}.peer-dot.connected{background:var(--text)}.new-pin-btn{margin:12px 20px;padding:10px;border:1px solid var(--border-strong);background:transparent;color:var(--text-muted);font-size:11px;letter-spacing:.2em;width:calc(100% - 40px);transition:all .2s;position:relative;overflow:hidden}.new-pin-btn:before{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:var(--text);transition:left .2s;z-index:-1}.new-pin-btn:hover:before{left:0}.new-pin-btn:hover{color:var(--bg);border-color:var(--text)}.panel{flex:1;display:flex;flex-direction:column;background:var(--bg);overflow:hidden;transition:background .3s}.welcome{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--border-strong);gap:12px;padding:40px;text-align:center}.welcome-logo{font-size:clamp(8px,1.5vw,14px);line-height:1.15;color:var(--border);animation:flicker 8s infinite}@keyframes flicker{0%,95%,to{opacity:1}96%{opacity:.4}97%{opacity:1}98%{opacity:.6}}.welcome-hint{font-size:11px;letter-spacing:.3em;color:var(--border-strong)}.chat-header{padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-shrink:0}.chat-name{font-family:var(--sans);font-size:13px;font-weight:400;flex:1}.chat-pin-label{font-size:10px;color:var(--text-muted);letter-spacing:.2em}.chat-menu-btn{background:none;border:none;color:var(--text-muted);font-size:13px;letter-spacing:.1em;padding:4px;transition:color .2s}.chat-menu-btn:hover{color:var(--text)}.messages-area{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px}.msg{max-width:75%;display:flex;flex-direction:column}.msg.mine{align-self:flex-end;align-items:flex-end}.msg.theirs{align-self:flex-start;align-items:flex-start}.msg-bubble{font-family:var(--sans);font-size:13px;font-weight:300;line-height:1.6;padding:8px 12px;border:1px solid var(--border);border-radius:0;transition:border-color .3s,background .3s}.msg.mine .msg-bubble{border-color:var(--border-strong)}.msg.theirs .msg-bubble{background:var(--surface);color:var(--text-sec)}.msg-time{font-size:10px;color:var(--border-strong);margin-top:3px}.msg-status-queued{opacity:.5;font-style:italic}.msg-status-delivered{opacity:.7}.chat-input-area{padding:10px 16px 14px;border-top:1px solid var(--border);display:flex;gap:8px;align-items:flex-end;flex-shrink:0}.chat-input{flex:1;background:var(--surface);border:1px solid var(--border);color:var(--text);font-size:13px;font-weight:300;padding:8px 12px;resize:none;outline:none;min-height:38px;max-height:100px;transition:border-color .2s;line-height:1.5;border-radius:0}.chat-input:focus{border-color:var(--border-strong)}.chat-input::placeholder{color:var(--border-strong)}.send-btn{width:38px;height:38px;border:1px solid var(--border-strong);background:transparent;color:var(--text-muted);font-size:12px;flex-shrink:0;transition:all .2s}.send-btn:hover{border-color:var(--text);color:var(--text)}.newpin-panel{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:20px;max-width:480px}.pin-display{border:1px solid var(--border);padding:28px;text-align:center;position:relative}.pin-display:before{content:"YOUR NEW PIN";position:absolute;top:-.5rem;left:1rem;background:var(--bg);padding:0 6px;font-size:10px;color:var(--border-strong);letter-spacing:.3em}.pin-value{font-size:2.5rem;letter-spacing:.3em;color:var(--text);margin-bottom:6px}.pin-hint{font-family:var(--sans);font-size:11px;color:var(--text-muted);font-weight:300}.form-group{display:flex;flex-direction:column;gap:6px}.form-label{font-size:10px;color:var(--border-strong);letter-spacing:.3em}.form-input{background:var(--surface);border:1px solid var(--border);color:var(--text);font-size:13px;padding:10px 12px;outline:none;width:100%;transition:border-color .2s;border-radius:0}.form-input:focus{border-color:var(--border-strong)}.form-input::placeholder{color:var(--border-strong)}.expiry-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:2px;background:var(--border)}.expiry-opt{background:var(--bg);padding:10px 6px;font-size:11px;color:var(--text-muted);text-align:center;letter-spacing:.05em;border:none;transition:all .2s}.expiry-opt:hover,.expiry-opt.selected{background:var(--surface);color:var(--text)}.expiry-opt.selected{box-shadow:inset 0 0 0 1px var(--border-strong)}.primary-btn{font-size:12px;padding:12px;border:1px solid var(--text);background:transparent;color:var(--text);letter-spacing:.2em;transition:all .2s;position:relative;overflow:hidden;width:100%}.primary-btn:before{content:"";position:absolute;top:0;left:-100%;width:100%;height:100%;background:var(--text);transition:left .2s;z-index:-1}.primary-btn:hover:before{left:0}.primary-btn:hover{color:var(--bg)}.ghost-btn{font-size:12px;padding:12px;border:1px solid var(--border-strong);background:transparent;color:var(--text-muted);letter-spacing:.2em;transition:all .2s;width:100%}.ghost-btn:hover{border-color:var(--text);color:var(--text)}.menu-panel{flex:1;overflow-y:auto;max-width:480px}.menu-pin-header{padding:16px 24px;border-bottom:1px solid var(--border)}.menu-pin-name{font-family:var(--sans);font-size:14px;font-weight:400;margin-bottom:2px}.menu-pin-sub{font-size:10px;color:var(--text-muted);letter-spacing:.2em}.menu-section-label{font-size:10px;color:var(--border-strong);letter-spacing:.4em;padding:16px 24px 6px}.menu-item{padding:16px 24px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;cursor:crosshair;transition:background .15s}.menu-item:hover{background:var(--surface)}.menu-item-label{font-size:12px;letter-spacing:.05em;margin-bottom:2px}.menu-item-sub{font-family:var(--sans);font-size:11px;color:var(--text-muted);font-weight:300}.menu-item-arrow{font-size:12px;color:var(--border-strong)}.menu-item.danger .menu-item-label{color:var(--text-muted)}.menu-item.danger:hover .menu-item-label{color:var(--text)}::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border-strong)}@media (max-width: 600px){.sidebar{width:100%;border-right:none}.main-area{flex-direction:column}.panel{display:none}.panel.active{display:flex}}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))s(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const m of o.addedNodes)m.tagName==="LINK"&&m.rel==="modulepreload"&&s(m)}).observe(document,{childList:!0,subtree:!0});function i(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(a){if(a.ep)return;a.ep=!0;const o=i(a);fetch(a.href,o)}})();class B{constructor(){this.ws=null,this.handlers=new Map,this.reconnectTimer=null}connect(){this.ws=new WebSocket(`ws://localhost:${location.port||3e3}`),this.ws.onmessage=t=>{try{const i=JSON.parse(t.data);(this.handlers.get(i.event)||[]).forEach(a=>a(i))}catch{}},this.ws.onclose=()=>{this.reconnectTimer=setTimeout(()=>this.connect(),2e3)},this.ws.onerror=()=>{var t;(t=this.ws)==null||t.close()}}on(t,i){this.handlers.has(t)||this.handlers.set(t,[]),this.handlers.get(t).push(i)}send(t,i={}){var s;((s=this.ws)==null?void 0:s.readyState)===WebSocket.OPEN&&this.ws.send(JSON.stringify({cmd:t,...i}))}destroy(){var t;this.reconnectTimer&&clearTimeout(this.reconnectTimer),(t=this.ws)==null||t.close()}}const c=new B,e={number:"",inbox:[],contacts:[],activePin:null,activeContact:null,messages:{},contactMessages:{},peerStatus:{},theme:"dark",screen:"welcome",newPinValue:g(),newPinExpiry:"none"};function g(){return Math.floor(Math.random()*65536).toString(16).padStart(4,"0")}function d(){const n=document.getElementById("app");n.innerHTML=T(),K()}function T(){return`
|
|
2
|
+
<div class="topbar">
|
|
3
|
+
<span class="topbar-logo">0x0</span>
|
|
4
|
+
<button class="theme-toggle" id="theme-toggle">
|
|
5
|
+
// ${e.theme==="dark"?"dark":"light"}
|
|
6
|
+
</button>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="main-area">
|
|
9
|
+
${D()}
|
|
10
|
+
<div class="panel ${e.screen!=="welcome"?"active":""}">
|
|
11
|
+
${A()}
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
`}function D(){const n=e.inbox,t=e.contacts;return`
|
|
15
|
+
<div class="sidebar">
|
|
16
|
+
<div class="number-section">
|
|
17
|
+
<div class="section-label">// my_number</div>
|
|
18
|
+
<div class="my-number" id="my-number">${e.number||"..."}</div>
|
|
19
|
+
<div class="number-actions">
|
|
20
|
+
<button class="pill-btn" id="btn-copy">// COPY</button>
|
|
21
|
+
<button class="pill-btn" id="btn-renew">// RENEW</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="inbox-section">
|
|
25
|
+
<div style="padding: 12px 20px 6px">
|
|
26
|
+
<div class="section-label">// inbox</div>
|
|
27
|
+
</div>
|
|
28
|
+
${n.map(i=>O(i)).join("")}
|
|
29
|
+
</div>
|
|
30
|
+
<button class="new-pin-btn" id="btn-new-pin">+ // NEW_PIN</button>
|
|
31
|
+
<div class="contacts-section">
|
|
32
|
+
<div style="padding: 12px 20px 6px; display:flex; justify-content:space-between; align-items:center">
|
|
33
|
+
<div class="section-label">// contacts</div>
|
|
34
|
+
<button class="pill-btn" id="btn-connect">+ CONNECT</button>
|
|
35
|
+
</div>
|
|
36
|
+
${t.length===0?'<div style="padding:8px 20px; font-size:11px; opacity:0.4">// no contacts yet</div>':t.map(i=>_(i)).join("")}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
`}function O(n){var o;const t=((o=e.activePin)==null?void 0:o.id)===n.id&&e.screen==="chat",i=e.peerStatus[n.id]||"disconnected",s=n.latest?L(n.latest.timestamp):"",a=n.latest?n.latest.content.slice(0,35)+(n.latest.content.length>35?"…":""):"(no messages)";return`
|
|
40
|
+
<div class="inbox-item ${t?"active":""}" data-pin-id="${n.id}">
|
|
41
|
+
<div class="peer-dot ${i==="connected"?"connected":""}"></div>
|
|
42
|
+
<div class="inbox-pin">${n.value}</div>
|
|
43
|
+
<div class="inbox-info">
|
|
44
|
+
<div class="inbox-label">${n.label||"(no label)"}</div>
|
|
45
|
+
<div class="inbox-preview">${a}</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="inbox-meta">
|
|
48
|
+
<div class="inbox-time">${s}</div>
|
|
49
|
+
<div class="inbox-badge" data-count="${n.unread}">${n.unread||""}</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
`}function _(n){var o;const t=((o=e.activeContact)==null?void 0:o.id)===n.id&&e.screen==="chat",i=e.peerStatus[n.id]||"disconnected",s=n.latest?L(n.latest.timestamp):"",a=n.latest?n.latest.content.slice(0,35)+(n.latest.content.length>35?"…":""):"(no messages)";return`
|
|
53
|
+
<div class="inbox-item ${t?"active":""}" data-contact-id="${n.id}">
|
|
54
|
+
<div class="peer-dot ${i==="connected"?"connected":""}"></div>
|
|
55
|
+
<div class="inbox-pin">${n.theirPin}</div>
|
|
56
|
+
<div class="inbox-info">
|
|
57
|
+
<div class="inbox-label">${n.label||n.theirNumber}</div>
|
|
58
|
+
<div class="inbox-preview">${k(a)}</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="inbox-meta">
|
|
61
|
+
<div class="inbox-time">${s}</div>
|
|
62
|
+
<div class="inbox-badge" data-count="${n.unread}">${n.unread||""}</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
`}function A(){switch(e.screen){case"welcome":return q();case"chat":return j();case"newPin":return V();case"pinMenu":return J();case"connect":return H()}}function q(){return`
|
|
66
|
+
<div class="welcome">
|
|
67
|
+
<pre class="welcome-logo">
|
|
68
|
+
██████╗ ██╗ ██╗██████╗
|
|
69
|
+
██╔═══██╗╚██╗██╔╝██╔══██╗
|
|
70
|
+
██║ ██║ ╚███╔╝ ██║ ██║
|
|
71
|
+
██║ ██║ ██╔██╗ ██║ ██║
|
|
72
|
+
╚██████╔╝██╔╝ ██╗██████╔╝
|
|
73
|
+
╚═════╝ ╚═╝ ╚═╝╚═════╝</pre>
|
|
74
|
+
<div class="welcome-hint">// select a pin from inbox to start chatting</div>
|
|
75
|
+
</div>
|
|
76
|
+
`}function j(){const n=e.activePin,t=e.activeContact,i=n?e.messages[n.id]||[]:t?e.contactMessages[t.id]||[]:[],s=n?n.id:(t==null?void 0:t.id)||"",a=e.peerStatus[s]||"disconnected",o=n?n.label||n.value:t?t.label||t.theirNumber:"",m=n?`// pin: ${n.value} · ${a}`:t?`// ${t.theirNumber} · pin: ${t.theirPin} · ${a}`:"";return`
|
|
77
|
+
<div style="display:flex;flex-direction:column;height:100%">
|
|
78
|
+
<div class="chat-header">
|
|
79
|
+
<div style="flex:1">
|
|
80
|
+
<div class="chat-name">${o}</div>
|
|
81
|
+
<div class="chat-pin-label">${m}</div>
|
|
82
|
+
</div>
|
|
83
|
+
${n?'<button class="chat-menu-btn" id="btn-pin-menu">⋯</button>':'<button class="chat-menu-btn" id="btn-contact-remove" title="削除">✕</button>'}
|
|
84
|
+
</div>
|
|
85
|
+
<div class="messages-area" id="messages-area">
|
|
86
|
+
${i.map(b=>W(b)).join("")}
|
|
87
|
+
</div>
|
|
88
|
+
<div class="chat-input-area">
|
|
89
|
+
<textarea
|
|
90
|
+
class="chat-input"
|
|
91
|
+
id="chat-input"
|
|
92
|
+
placeholder="メッセージ..."
|
|
93
|
+
rows="1"
|
|
94
|
+
></textarea>
|
|
95
|
+
<button class="send-btn" id="btn-send">▶</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
`}function W(n){const t=n.isMine?"mine":"theirs",i=new Date(n.timestamp).toLocaleTimeString("ja-JP",{hour:"2-digit",minute:"2-digit"});let s="";return n.isMine&&(n.status==="queued"?s='<span class="msg-status-queued"> // waiting…</span>':n.status==="delivered"&&(s='<span class="msg-status-delivered"> ✓</span>')),`
|
|
99
|
+
<div class="msg ${t}" data-local-id="${n.localId||""}">
|
|
100
|
+
<div class="msg-bubble">${k(n.content)}</div>
|
|
101
|
+
<div class="msg-time">${i}${s}</div>
|
|
102
|
+
</div>
|
|
103
|
+
`}function V(){const n=[{value:"none",label:"なし"},{value:"24h",label:"24時間"},{value:"1w",label:"1週間"},{value:"once",label:"1回のみ"}];return`
|
|
104
|
+
<div class="newpin-panel">
|
|
105
|
+
<div class="section-label">// new_pin</div>
|
|
106
|
+
<div class="pin-display">
|
|
107
|
+
<div class="pin-value" id="new-pin-display">${e.newPinValue}</div>
|
|
108
|
+
<div class="pin-hint">相手に渡すPINです</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="form-group">
|
|
111
|
+
<div class="form-label">// label(任意)</div>
|
|
112
|
+
<input class="form-input" id="pin-label-input" type="text" placeholder="例: フリマ用、田中さん...">
|
|
113
|
+
</div>
|
|
114
|
+
<div class="form-group">
|
|
115
|
+
<div class="form-label">// expiry</div>
|
|
116
|
+
<div class="expiry-grid">
|
|
117
|
+
${n.map(t=>`
|
|
118
|
+
<button class="expiry-opt ${e.newPinExpiry===t.value?"selected":""}"
|
|
119
|
+
data-expiry="${t.value}">${t.label}</button>
|
|
120
|
+
`).join("")}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<button class="ghost-btn" id="btn-regen-pin">// GENERATE_NEW →</button>
|
|
124
|
+
<button class="primary-btn" id="btn-save-pin">// SAVE_AND_USE →</button>
|
|
125
|
+
</div>
|
|
126
|
+
`}function H(){return`
|
|
127
|
+
<div class="newpin-panel">
|
|
128
|
+
<div class="section-label">// connect_to_peer</div>
|
|
129
|
+
<div class="form-group">
|
|
130
|
+
<div class="form-label">// their_number</div>
|
|
131
|
+
<input class="form-input" id="connect-number" type="text" placeholder="0x0-NNN-NNNN-NNNN" autocomplete="off">
|
|
132
|
+
</div>
|
|
133
|
+
<div class="form-group">
|
|
134
|
+
<div class="form-label">// pin</div>
|
|
135
|
+
<input class="form-input" id="connect-pin" type="text" placeholder="a3f9" maxlength="4" autocomplete="off">
|
|
136
|
+
</div>
|
|
137
|
+
<div class="form-group">
|
|
138
|
+
<div class="form-label">// label(任意)</div>
|
|
139
|
+
<input class="form-input" id="connect-label" type="text" placeholder="例: 田中さん">
|
|
140
|
+
</div>
|
|
141
|
+
<button class="primary-btn" id="btn-do-connect">// CONNECT →</button>
|
|
142
|
+
</div>
|
|
143
|
+
`}function J(){const n=e.activePin;return`
|
|
144
|
+
<div class="menu-panel">
|
|
145
|
+
<div class="menu-pin-header">
|
|
146
|
+
<div class="menu-pin-name">${n.label||n.value}</div>
|
|
147
|
+
<div class="menu-pin-sub">// pin: ${n.value}</div>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="menu-section-label">// pin_settings</div>
|
|
150
|
+
<div class="menu-item" id="menu-rotate">
|
|
151
|
+
<div>
|
|
152
|
+
<div class="menu-item-label">PINを変更する</div>
|
|
153
|
+
<div class="menu-item-sub">新しいPINを生成して渡し直す</div>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="menu-item-arrow">›</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="menu-item" id="menu-label">
|
|
158
|
+
<div>
|
|
159
|
+
<div class="menu-item-label">ラベルを編集</div>
|
|
160
|
+
<div class="menu-item-sub">現在: ${n.label||"(なし)"}</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="menu-item-arrow">›</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="menu-section-label">// danger_zone</div>
|
|
165
|
+
<div class="menu-item danger" id="menu-revoke">
|
|
166
|
+
<div>
|
|
167
|
+
<div class="menu-item-label">このPINを今すぐ無効化</div>
|
|
168
|
+
<div class="menu-item-sub">相手からの受信が即時停止される</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="menu-item-arrow">›</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div style="padding: 16px 24px">
|
|
173
|
+
<button class="ghost-btn" id="menu-back">← back</button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
`}function K(){var i,s,a,o,m,b,h,f,y,I,x,w,E,P,$;(i=document.getElementById("theme-toggle"))==null||i.addEventListener("click",()=>{e.theme=e.theme==="dark"?"light":"dark",document.documentElement.classList.toggle("light",e.theme==="light"),d()}),(s=document.getElementById("btn-copy"))==null||s.addEventListener("click",()=>{navigator.clipboard.writeText(e.number).catch(()=>{})}),(a=document.getElementById("btn-renew"))==null||a.addEventListener("click",()=>{confirm("番号を再発行しますか?全てのPINが無効になります。")&&c.send("number.renew")}),(o=document.getElementById("btn-new-pin"))==null||o.addEventListener("click",()=>{e.screen="newPin",e.newPinValue=g(),d()}),(m=document.getElementById("btn-connect"))==null||m.addEventListener("click",()=>{e.screen="connect",e.activePin=null,e.activeContact=null,d()}),document.querySelectorAll(".inbox-item[data-pin-id]").forEach(l=>{l.addEventListener("click",()=>{const r=l.dataset.pinId,u=e.inbox.find(p=>p.id===r);u&&(e.activePin=u,e.activeContact=null,e.screen="chat",d(),c.send("messages.list",{pinId:r}),v())})}),document.querySelectorAll(".inbox-item[data-contact-id]").forEach(l=>{l.addEventListener("click",()=>{const r=l.dataset.contactId,u=e.contacts.find(p=>p.id===r);u&&(e.activeContact=u,e.activePin=null,e.screen="chat",d(),c.send("chat.start",{theirNumber:u.theirNumber,theirPin:u.theirPin,label:u.label}),v())})}),(b=document.getElementById("btn-pin-menu"))==null||b.addEventListener("click",()=>{e.screen="pinMenu",d()}),(h=document.getElementById("btn-contact-remove"))==null||h.addEventListener("click",()=>{e.activeContact&&confirm(`${e.activeContact.label||e.activeContact.theirNumber} を削除しますか?`)&&(c.send("contact.remove",{contactId:e.activeContact.id}),e.activeContact=null,e.screen="welcome",d())});const n=document.getElementById("chat-input"),t=()=>{const l=n==null?void 0:n.value.trim();if(l){if(e.activePin){const r=e.activePin.id,u=crypto.randomUUID(),p=Date.now();e.messages[r]||(e.messages[r]=[]),e.messages[r].push({localId:u,content:l,isMine:!0,timestamp:p,status:"queued"}),c.send("message.send",{pinId:r,content:l,localId:u}),d(),v()}else e.activeContact&&c.send("contact.message.send",{contactId:e.activeContact.id,content:l});n.value="",n.style.height="auto"}};(f=document.getElementById("btn-send"))==null||f.addEventListener("click",t),n==null||n.addEventListener("keydown",l=>{l.key==="Enter"&&!l.shiftKey&&(l.preventDefault(),t())}),n==null||n.addEventListener("input",()=>{n&&(n.style.height="auto",n.style.height=Math.min(n.scrollHeight,100)+"px")}),document.querySelectorAll(".expiry-opt").forEach(l=>{l.addEventListener("click",()=>{e.newPinExpiry=l.dataset.expiry||"none",d()})}),(y=document.getElementById("btn-regen-pin"))==null||y.addEventListener("click",()=>{e.newPinValue=g();const l=document.getElementById("new-pin-display");l&&S(l,e.newPinValue)}),(I=document.getElementById("btn-save-pin"))==null||I.addEventListener("click",()=>{var r;const l=((r=document.getElementById("pin-label-input"))==null?void 0:r.value.trim())||"";c.send("pin.create",{label:l,expiry:e.newPinExpiry}),e.screen="welcome",e.newPinExpiry="none",d()}),(x=document.getElementById("btn-do-connect"))==null||x.addEventListener("click",()=>{var p,N,C;const l=(p=document.getElementById("connect-number"))==null?void 0:p.value.trim(),r=(N=document.getElementById("connect-pin"))==null?void 0:N.value.trim(),u=((C=document.getElementById("connect-label"))==null?void 0:C.value.trim())||"";!l||!r||c.send("chat.start",{theirNumber:l,theirPin:r,label:u})}),(w=document.getElementById("menu-rotate"))==null||w.addEventListener("click",()=>{e.activePin&&(c.send("pin.rotate",{pinId:e.activePin.id}),e.screen="welcome",e.activePin=null,d())}),(E=document.getElementById("menu-revoke"))==null||E.addEventListener("click",()=>{e.activePin&&(c.send("pin.revoke",{pinId:e.activePin.id}),e.screen="welcome",e.activePin=null,d())}),(P=document.getElementById("menu-back"))==null||P.addEventListener("click",()=>{e.screen="chat",d()}),($=document.getElementById("menu-label"))==null||$.addEventListener("click",()=>{var l;prompt("新しいラベル:",((l=e.activePin)==null?void 0:l.label)||"")})}c.on("init",n=>{var i;const t=n;e.number=t.data.number,e.inbox=t.data.inbox,e.contacts=t.data.contacts||[],((i=t.data.prefs)==null?void 0:i.theme)==="light"&&(e.theme="light",document.documentElement.classList.add("light")),d(),M()});c.on("inbox.list",n=>{const t=n;if(e.inbox=t.data,e.activePin){const i=t.data.find(s=>s.id===e.activePin.id);i&&(e.activePin=i)}d()});c.on("contacts.list",n=>{const t=n;if(e.contacts=t.data,e.activeContact){const i=t.data.find(s=>s.id===e.activeContact.id);i&&(e.activeContact=i)}d()});c.on("messages.list",n=>{var i;const t=n;e.messages[t.pinId]=t.data,e.screen==="chat"&&((i=e.activePin)==null?void 0:i.id)===t.pinId&&(d(),v())});c.on("chat.started",n=>{const t=n,i=e.contacts.findIndex(s=>s.id===t.contactId);i===-1?e.contacts.push(t.data):e.contacts[i]={...e.contacts[i],...t.data},e.activeContact=e.contacts.find(s=>s.id===t.contactId)||t.data,e.activePin=null,e.screen="chat",d(),v()});c.on("contact.messages.list",n=>{var i;const t=n;e.contactMessages[t.contactId]=t.data,e.screen==="chat"&&((i=e.activeContact)==null?void 0:i.id)===t.contactId&&(d(),v())});c.on("message.received",n=>{var s;const t=n;e.messages[t.pinId]||(e.messages[t.pinId]=[]),e.messages[t.pinId].push(t.data);const i=e.inbox.find(a=>a.id===t.pinId);i&&(i.unread++,i.latest=t.data),e.screen==="chat"&&((s=e.activePin)==null?void 0:s.id)===t.pinId?(d(),v()):d()});c.on("contact.message.received",n=>{var s;const t=n;e.contactMessages[t.contactId]||(e.contactMessages[t.contactId]=[]),e.contactMessages[t.contactId].push(t.data);const i=e.contacts.find(a=>a.id===t.contactId);i&&(i.unread++,i.latest=t.data),e.screen==="chat"&&((s=e.activeContact)==null?void 0:s.id)===t.contactId?(d(),v()):d()});c.on("message.sent",n=>{var s;const t=n,i=e.messages[t.pinId]||[];if(t.localId){const a=i.findIndex(o=>o.localId===t.localId);a!==-1&&(i[a]={...i[a],status:"delivered"},e.messages[t.pinId]=i)}else e.messages[t.pinId]||(e.messages[t.pinId]=[]),e.messages[t.pinId].push(t.data);e.screen==="chat"&&((s=e.activePin)==null?void 0:s.id)===t.pinId&&(d(),v())});c.on("message.queued",n=>{});c.on("message.delivered",n=>{var a;const t=n,i=e.messages[t.pinId]||[],s=i.findIndex(o=>o.localId===t.localId);s!==-1&&(i[s]={...i[s],status:"delivered"},e.messages[t.pinId]=i,e.screen==="chat"&&((a=e.activePin)==null?void 0:a.id)===t.pinId&&d())});c.on("contact.message.sent",n=>{var i;const t=n;e.contactMessages[t.contactId]||(e.contactMessages[t.contactId]=[]),e.contactMessages[t.contactId].push(t.data),e.screen==="chat"&&((i=e.activeContact)==null?void 0:i.id)===t.contactId&&(d(),v())});c.on("peer.status",n=>{const t=n,i=t.pinId||t.contactId||"";i&&(e.peerStatus[i]=t.status),d()});c.on("pin.created",()=>{c.send("inbox.list")});c.on("pin.rotated",()=>{c.send("inbox.list")});c.on("pin.revoked",()=>{c.send("inbox.list")});c.on("contact.removed",n=>{const t=n;e.contacts=e.contacts.filter(i=>i.id!==t.contactId),delete e.contactMessages[t.contactId],delete e.peerStatus[t.contactId],d()});c.on("number.renewed",n=>{const t=n;e.number=t.data.number,d(),M()});function v(){setTimeout(()=>{const n=document.getElementById("messages-area");n&&(n.scrollTop=n.scrollHeight)},0)}function L(n){const t=new Date(n),i=new Date;if(t.toDateString()===i.toDateString())return t.toLocaleTimeString("ja-JP",{hour:"2-digit",minute:"2-digit"});const s=new Date(i);return s.setDate(s.getDate()-1),t.toDateString()===s.toDateString()?"昨日":t.toLocaleDateString("ja-JP",{month:"numeric",day:"numeric"})}function k(n){return n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\n/g,"<br>")}function S(n,t){const i="0123456789abcdef";let s=0;const a=setInterval(()=>{let o="";for(const m of t)o+=Math.random()<.3?i[Math.floor(Math.random()*i.length)]:m;n.textContent=o,++s>6&&(clearInterval(a),n.textContent=t)},70)}function M(){const n=document.getElementById("my-number");n&&e.number&&S(n,e.number)}c.connect();d();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>0x0</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Noto+Sans+JP:wght@300;400&display=swap" rel="stylesheet">
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-Ci89CtgQ.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BrxSM83h.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="app"></div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "0x0-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "P2P disposable number messenger",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"0x0": "./bin/0x0.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"dist/web/"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node bin/0x0.js",
|
|
16
|
+
"build:web": "vite build web-ui",
|
|
17
|
+
"dev:web": "vite web-ui"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"chalk": "^5.3.0",
|
|
21
|
+
"commander": "^12.0.0",
|
|
22
|
+
"express": "^4.18.0",
|
|
23
|
+
"hyperswarm": "^4.7.0",
|
|
24
|
+
"open": "^10.1.0",
|
|
25
|
+
"ora": "^8.0.1",
|
|
26
|
+
"qrcode-terminal": "^0.12.0",
|
|
27
|
+
"uuid": "^9.0.0",
|
|
28
|
+
"ws": "^8.16.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.3.0",
|
|
32
|
+
"vite": "^5.1.0"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import readline from 'readline'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import Hyperswarm from 'hyperswarm'
|
|
5
|
+
import { channelSecret } from '../core/channel.js'
|
|
6
|
+
import { createMessage, parseMessage } from '../core/message.js'
|
|
7
|
+
import { generatePin } from '../core/pin.js'
|
|
8
|
+
import * as identityStore from '../storage/identity.js'
|
|
9
|
+
import * as pinsStore from '../storage/pins.js'
|
|
10
|
+
import * as messagesStore from '../storage/messages.js'
|
|
11
|
+
import * as contactsStore from '../storage/contacts.js'
|
|
12
|
+
|
|
13
|
+
export async function cmdChat(theirNumber, pin) {
|
|
14
|
+
const identity = identityStore.load()
|
|
15
|
+
if (!identity) {
|
|
16
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const localPin = pinsStore.findByValue(pin)
|
|
21
|
+
const swarm = new Hyperswarm()
|
|
22
|
+
const topic = channelSecret(theirNumber, pin)
|
|
23
|
+
let activeConn = null
|
|
24
|
+
let rl = null
|
|
25
|
+
|
|
26
|
+
const spinner = ora({
|
|
27
|
+
text: chalk.gray(`connecting to ${theirNumber}...`),
|
|
28
|
+
color: 'white'
|
|
29
|
+
}).start()
|
|
30
|
+
|
|
31
|
+
swarm.join(topic, { server: true, client: true })
|
|
32
|
+
|
|
33
|
+
swarm.on('connection', (conn) => {
|
|
34
|
+
if (activeConn) return // 既に接続済みなら無視
|
|
35
|
+
activeConn = conn
|
|
36
|
+
|
|
37
|
+
// 公開鍵で連絡先を自動識別・保存
|
|
38
|
+
const pubKeyHex = conn.remotePublicKey?.toString('hex') ?? null
|
|
39
|
+
if (pubKeyHex) {
|
|
40
|
+
let c = contactsStore.findByPublicKey(pubKeyHex)
|
|
41
|
+
if (!c) {
|
|
42
|
+
c = contactsStore.create({ theirNumber, theirPin: pin, peerPublicKey: pubKeyHex })
|
|
43
|
+
} else if (!c.peerPublicKey) {
|
|
44
|
+
contactsStore.updatePublicKey(c.id, pubKeyHex)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
spinner.stop()
|
|
49
|
+
printHeader(pin)
|
|
50
|
+
|
|
51
|
+
conn.on('error', () => {})
|
|
52
|
+
conn.on('data', (data) => {
|
|
53
|
+
const msg = parseMessage(data)
|
|
54
|
+
if (!msg || msg.type !== 'message') return
|
|
55
|
+
|
|
56
|
+
const time = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
|
|
57
|
+
// readline の入力行をクリアして上書き表示
|
|
58
|
+
if (rl) process.stdout.write('\r\x1b[K')
|
|
59
|
+
console.log(
|
|
60
|
+
chalk.gray(`[${time}]`) + ' ' +
|
|
61
|
+
chalk.hex('#888888')(theirNumber.slice(0, 16) + '…: ') +
|
|
62
|
+
chalk.hex('#aaaaaa')(msg.content)
|
|
63
|
+
)
|
|
64
|
+
if (rl) rl.prompt(true)
|
|
65
|
+
|
|
66
|
+
if (localPin) {
|
|
67
|
+
messagesStore.append(localPin.value, {
|
|
68
|
+
from: theirNumber, content: msg.content, isMine: false
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
conn.on('close', () => {
|
|
74
|
+
console.log()
|
|
75
|
+
console.log(chalk.gray('[peer disconnected]'))
|
|
76
|
+
activeConn = null
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (!rl) rl = startRepl()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// 3秒後もつながっていなければ待機状態でREPLを開始
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
if (!activeConn) {
|
|
85
|
+
spinner.stop()
|
|
86
|
+
console.log(chalk.gray('[waiting for peer...]'))
|
|
87
|
+
printHeader(pin)
|
|
88
|
+
if (!rl) rl = startRepl()
|
|
89
|
+
}
|
|
90
|
+
}, 3000)
|
|
91
|
+
|
|
92
|
+
function printHeader(p) {
|
|
93
|
+
console.log()
|
|
94
|
+
console.log(chalk.gray(`// pin: ${p} | type :help for commands`))
|
|
95
|
+
console.log()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function startRepl() {
|
|
99
|
+
const r = readline.createInterface({
|
|
100
|
+
input: process.stdin,
|
|
101
|
+
output: process.stdout,
|
|
102
|
+
prompt: chalk.gray('> ')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
r.prompt()
|
|
106
|
+
|
|
107
|
+
r.on('line', async (line) => {
|
|
108
|
+
const input = line.trim()
|
|
109
|
+
if (!input) { r.prompt(); return }
|
|
110
|
+
|
|
111
|
+
if (input.startsWith(':')) {
|
|
112
|
+
await handleCommand(input.slice(1).trim(), r)
|
|
113
|
+
r.prompt()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!activeConn) {
|
|
118
|
+
console.log(chalk.gray('// not connected yet'))
|
|
119
|
+
r.prompt()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
activeConn.write(createMessage(input))
|
|
124
|
+
|
|
125
|
+
const time = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
|
|
126
|
+
process.stdout.write('\x1b[1A\x1b[K')
|
|
127
|
+
console.log(chalk.gray(`[${time}]`) + chalk.gray(' you: ') + chalk.white(input))
|
|
128
|
+
|
|
129
|
+
if (localPin) {
|
|
130
|
+
messagesStore.append(localPin.value, {
|
|
131
|
+
from: identity.number, content: input, isMine: true
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
r.prompt()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
r.on('close', async () => {
|
|
139
|
+
console.log(chalk.gray('[disconnected]'))
|
|
140
|
+
await swarm.destroy()
|
|
141
|
+
process.exit(0)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return r
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function handleCommand(cmd, r) {
|
|
148
|
+
switch (cmd) {
|
|
149
|
+
case 'help':
|
|
150
|
+
console.log(chalk.gray(':pin rotate — change pin and get new one'))
|
|
151
|
+
console.log(chalk.gray(':pin info — show current pin info'))
|
|
152
|
+
console.log(chalk.gray(':history — show message history'))
|
|
153
|
+
console.log(chalk.gray(':clear — clear screen'))
|
|
154
|
+
console.log(chalk.gray(':quit / :q — disconnect and exit'))
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
case 'pin rotate':
|
|
158
|
+
if (localPin) {
|
|
159
|
+
const newValue = generatePin()
|
|
160
|
+
pinsStore.rotate(localPin.id, newValue)
|
|
161
|
+
console.log(chalk.gray('rotating pin...'))
|
|
162
|
+
console.log(chalk.gray('new pin: ') + chalk.hex('#aaaaaa')(newValue))
|
|
163
|
+
console.log(chalk.gray('// share the new pin with your contact to continue'))
|
|
164
|
+
} else {
|
|
165
|
+
console.log(chalk.gray('// pin not found in your list'))
|
|
166
|
+
}
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
case 'pin info':
|
|
170
|
+
console.log(chalk.gray(`pin: ${pin}`))
|
|
171
|
+
if (localPin) console.log(chalk.gray(`label: ${localPin.label || '(none)'}`))
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
case 'history':
|
|
175
|
+
if (localPin) {
|
|
176
|
+
const msgs = messagesStore.list(localPin.value)
|
|
177
|
+
if (msgs.length === 0) {
|
|
178
|
+
console.log(chalk.gray('(no history)'))
|
|
179
|
+
} else {
|
|
180
|
+
for (const msg of msgs) {
|
|
181
|
+
const t = new Date(msg.timestamp).toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
|
|
182
|
+
const prefix = msg.isMine
|
|
183
|
+
? chalk.gray(`[${t}] you: `)
|
|
184
|
+
: chalk.gray(`[${t}] them: `)
|
|
185
|
+
console.log(prefix + chalk.white(msg.content))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
case 'clear':
|
|
192
|
+
process.stdout.write('\x1b[2J\x1b[0f')
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
case 'quit':
|
|
196
|
+
case 'q':
|
|
197
|
+
r.close()
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
console.log(chalk.gray(`// unknown command: :${cmd}`))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|