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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { parseUri } from '../core/uri.js'
|
|
3
|
+
import * as contactsStore from '../storage/contacts.js'
|
|
4
|
+
|
|
5
|
+
function log(msg) {
|
|
6
|
+
console.log(chalk.gray(msg))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatContact(c) {
|
|
10
|
+
const short = c.id.slice(0, 8)
|
|
11
|
+
const label = c.label ? ` [${c.label}]` : ''
|
|
12
|
+
const key = c.peerPublicKey ? ` key:${c.peerPublicKey.slice(0, 8)}…` : ''
|
|
13
|
+
return `${short} ${c.theirNumber}/${c.theirPin}${label}${key}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function cmdContactAdd(uriOrNumber, pin, opts = {}) {
|
|
17
|
+
const parsed = parseUri(uriOrNumber)
|
|
18
|
+
const theirNumber = parsed ? parsed.number : uriOrNumber
|
|
19
|
+
const theirPin = parsed ? parsed.pin : pin
|
|
20
|
+
|
|
21
|
+
if (!theirNumber || !theirPin) {
|
|
22
|
+
log('// usage: contact add <uri> or contact add <number> <pin>')
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const contact = contactsStore.create({
|
|
27
|
+
theirNumber,
|
|
28
|
+
theirPin,
|
|
29
|
+
label: opts.label || ''
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
log(`// saved: ${formatContact(contact)}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function cmdContactList() {
|
|
36
|
+
const contacts = contactsStore.loadAll()
|
|
37
|
+
if (contacts.length === 0) {
|
|
38
|
+
log('// no contacts')
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log(`// ${contacts.length} contact${contacts.length === 1 ? '' : 's'}`)
|
|
43
|
+
for (const c of contacts) {
|
|
44
|
+
log(formatContact(c))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function cmdContactLabel(id, label) {
|
|
49
|
+
if (!contactsStore.updateLabel(id, label)) {
|
|
50
|
+
log(`// not found: ${id}`)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
log(`// updated: ${id.slice(0, 8)} label → ${label}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function cmdContactRemove(id) {
|
|
57
|
+
if (!contactsStore.findById(id)) {
|
|
58
|
+
log(`// not found: ${id}`)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
contactsStore.remove(id)
|
|
62
|
+
log(`// removed: ${id.slice(0, 8)}`)
|
|
63
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import * as identityStore from '../storage/identity.js'
|
|
3
|
+
import * as pinsStore from '../storage/pins.js'
|
|
4
|
+
import * as messagesStore from '../storage/messages.js'
|
|
5
|
+
|
|
6
|
+
function formatTime(ts) {
|
|
7
|
+
const d = new Date(ts)
|
|
8
|
+
const now = new Date()
|
|
9
|
+
if (d.toDateString() === now.toDateString()) {
|
|
10
|
+
return d.toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
|
|
11
|
+
}
|
|
12
|
+
const yesterday = new Date(now)
|
|
13
|
+
yesterday.setDate(yesterday.getDate() - 1)
|
|
14
|
+
if (d.toDateString() === yesterday.toDateString()) return '昨日'
|
|
15
|
+
return d.toLocaleDateString('ja-JP', { month: 'numeric', day: 'numeric' })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function cmdInbox({ json = false } = {}) {
|
|
19
|
+
const identity = identityStore.load()
|
|
20
|
+
if (!identity) {
|
|
21
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pins = pinsStore.getActive()
|
|
26
|
+
|
|
27
|
+
if (json) {
|
|
28
|
+
const result = pins.map(pin => {
|
|
29
|
+
const msgs = messagesStore.list(pin.value)
|
|
30
|
+
const latest = msgs[msgs.length - 1] || null
|
|
31
|
+
return { pin: pin.value, label: pin.label, messageCount: msgs.length, latest }
|
|
32
|
+
})
|
|
33
|
+
console.log(JSON.stringify(result, null, 2))
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(chalk.gray('// my_number'))
|
|
38
|
+
console.log(chalk.white(identity.number))
|
|
39
|
+
console.log()
|
|
40
|
+
console.log(chalk.gray('// inbox'))
|
|
41
|
+
console.log(chalk.gray('─'.repeat(52)))
|
|
42
|
+
|
|
43
|
+
if (pins.length === 0) {
|
|
44
|
+
console.log(chalk.gray(' (empty) run: 0x0 pin new --label "someone"'))
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const pin of pins) {
|
|
49
|
+
const msgs = messagesStore.list(pin.value)
|
|
50
|
+
const latest = msgs[msgs.length - 1]
|
|
51
|
+
const count = msgs.length
|
|
52
|
+
|
|
53
|
+
const label = (pin.label || '').padEnd(14)
|
|
54
|
+
const preview = latest
|
|
55
|
+
? latest.content.slice(0, 38) + (latest.content.length > 38 ? '…' : '')
|
|
56
|
+
: chalk.gray('(no messages)')
|
|
57
|
+
const time = latest ? formatTime(latest.timestamp) : ''
|
|
58
|
+
const badge = count > 0 ? chalk.white(`(${count})`) : chalk.gray('(0)')
|
|
59
|
+
|
|
60
|
+
console.log(
|
|
61
|
+
` ${chalk.hex('#aaaaaa')(pin.value.padEnd(6))}` +
|
|
62
|
+
` ${chalk.white(label)}` +
|
|
63
|
+
` ${badge.padEnd(5)}` +
|
|
64
|
+
` ${chalk.gray(preview.slice(0, 36))}` +
|
|
65
|
+
(time ? ` ${chalk.gray(time)}` : '')
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { generateNumber } from '../core/identity.js'
|
|
3
|
+
import { generatePin } from '../core/pin.js'
|
|
4
|
+
import * as identityStore from '../storage/identity.js'
|
|
5
|
+
import * as pinsStore from '../storage/pins.js'
|
|
6
|
+
|
|
7
|
+
export async function cmdInit() {
|
|
8
|
+
const existing = identityStore.load()
|
|
9
|
+
if (existing) {
|
|
10
|
+
console.log(chalk.gray('// already initialized'))
|
|
11
|
+
console.log()
|
|
12
|
+
console.log(chalk.gray('your number ') + chalk.white(existing.number))
|
|
13
|
+
console.log(chalk.gray('// run: 0x0 pin list to see your pins'))
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
identityStore.ensureDir()
|
|
18
|
+
const number = generateNumber()
|
|
19
|
+
const pinValue = generatePin()
|
|
20
|
+
|
|
21
|
+
identityStore.save({ number, createdAt: Date.now() })
|
|
22
|
+
pinsStore.create({ value: pinValue, label: 'default' })
|
|
23
|
+
|
|
24
|
+
console.log(chalk.gray('// generating your number...'))
|
|
25
|
+
console.log()
|
|
26
|
+
console.log(chalk.gray('your number ') + chalk.white(number))
|
|
27
|
+
console.log(chalk.gray('your pin ') + chalk.hex('#aaaaaa')(pinValue))
|
|
28
|
+
console.log()
|
|
29
|
+
console.log(chalk.gray('// share your number and a pin with someone to start chatting'))
|
|
30
|
+
console.log(chalk.gray('// example: 0x0 chat <their-number> <pin>'))
|
|
31
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import Hyperswarm from 'hyperswarm'
|
|
3
|
+
import { channelSecret } from '../core/channel.js'
|
|
4
|
+
import { parseMessage } from '../core/message.js'
|
|
5
|
+
import * as identityStore from '../storage/identity.js'
|
|
6
|
+
import * as pinsStore from '../storage/pins.js'
|
|
7
|
+
import * as messagesStore from '../storage/messages.js'
|
|
8
|
+
import * as contactsStore from '../storage/contacts.js'
|
|
9
|
+
|
|
10
|
+
export async function cmdListen({ pin: pinFilter } = {}) {
|
|
11
|
+
const identity = identityStore.load()
|
|
12
|
+
if (!identity) {
|
|
13
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pins = pinFilter
|
|
18
|
+
? [pinsStore.findByValue(pinFilter)].filter(Boolean)
|
|
19
|
+
: pinsStore.getActive()
|
|
20
|
+
|
|
21
|
+
if (pins.length === 0) {
|
|
22
|
+
console.log(chalk.gray('// no active pins'))
|
|
23
|
+
process.exit(0)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(chalk.gray(`// listening on ${pins.length} pin(s)...`))
|
|
27
|
+
console.log(chalk.gray('// ctrl+c to stop'))
|
|
28
|
+
console.log()
|
|
29
|
+
|
|
30
|
+
const swarms = []
|
|
31
|
+
|
|
32
|
+
for (const pin of pins) {
|
|
33
|
+
const swarm = new Hyperswarm()
|
|
34
|
+
const topic = channelSecret(identity.number, pin.value)
|
|
35
|
+
swarms.push(swarm)
|
|
36
|
+
|
|
37
|
+
swarm.join(topic, { server: true, client: true })
|
|
38
|
+
|
|
39
|
+
swarm.on('connection', (conn) => {
|
|
40
|
+
conn.on('error', () => {})
|
|
41
|
+
|
|
42
|
+
// 公開鍵で連絡先を自動識別・保存(送信者番号は不明なので 'unknown')
|
|
43
|
+
const pubKeyHex = conn.remotePublicKey?.toString('hex') ?? null
|
|
44
|
+
if (pubKeyHex) {
|
|
45
|
+
let c = contactsStore.findByPublicKey(pubKeyHex)
|
|
46
|
+
if (!c) {
|
|
47
|
+
c = contactsStore.create({ theirNumber: 'unknown', theirPin: pin.value, peerPublicKey: pubKeyHex })
|
|
48
|
+
} else if (!c.peerPublicKey) {
|
|
49
|
+
contactsStore.updatePublicKey(c.id, pubKeyHex)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
conn.on('data', (data) => {
|
|
54
|
+
const msg = parseMessage(data)
|
|
55
|
+
if (!msg || msg.type !== 'message') return
|
|
56
|
+
|
|
57
|
+
messagesStore.append(pin.value, {
|
|
58
|
+
from: 'peer', content: msg.content, isMine: false
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const time = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
|
|
62
|
+
const label = pin.label ? `[${pin.label}]` : `[${pin.value}]`
|
|
63
|
+
console.log(chalk.gray(`[${time}] ${label} `) + chalk.hex('#aaaaaa')(msg.content))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.on('SIGINT', async () => {
|
|
69
|
+
console.log()
|
|
70
|
+
console.log(chalk.gray('// stopping...'))
|
|
71
|
+
for (const swarm of swarms) await swarm.destroy()
|
|
72
|
+
process.exit(0)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { generatePin } from '../core/pin.js'
|
|
3
|
+
import * as pinsStore from '../storage/pins.js'
|
|
4
|
+
import * as identityStore from '../storage/identity.js'
|
|
5
|
+
|
|
6
|
+
function requireInit() {
|
|
7
|
+
const identity = identityStore.load()
|
|
8
|
+
if (!identity) {
|
|
9
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
return identity
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseExpiry(expires, once) {
|
|
16
|
+
if (once) return { expiry: 'once', expiresAt: null }
|
|
17
|
+
if (!expires) return { expiry: 'none', expiresAt: null }
|
|
18
|
+
|
|
19
|
+
const match = expires.match(/^(\d+)(h|d|w)$/)
|
|
20
|
+
if (!match) return { expiry: expires, expiresAt: null }
|
|
21
|
+
|
|
22
|
+
const num = parseInt(match[1])
|
|
23
|
+
const unit = match[2]
|
|
24
|
+
const ms = unit === 'h' ? num * 3_600_000
|
|
25
|
+
: unit === 'd' ? num * 86_400_000
|
|
26
|
+
: num * 7 * 86_400_000
|
|
27
|
+
return { expiry: expires, expiresAt: Date.now() + ms }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function cmdPinNew({ label = '', expires, once } = {}) {
|
|
31
|
+
requireInit()
|
|
32
|
+
|
|
33
|
+
const { expiry, expiresAt } = parseExpiry(expires, once)
|
|
34
|
+
const value = generatePin()
|
|
35
|
+
const pin = pinsStore.create({ value, label, expiry, expiresAt })
|
|
36
|
+
|
|
37
|
+
console.log(chalk.gray('// new pin created'))
|
|
38
|
+
console.log()
|
|
39
|
+
console.log(chalk.gray('pin ') + chalk.hex('#aaaaaa')(pin.value))
|
|
40
|
+
if (label) console.log(chalk.gray('label ') + chalk.white(label))
|
|
41
|
+
if (expiry !== 'none') console.log(chalk.gray('expiry ') + chalk.gray(expiry))
|
|
42
|
+
if (expiresAt) console.log(chalk.gray('expires ') + chalk.gray(new Date(expiresAt).toLocaleString('ja-JP')))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function cmdPinList() {
|
|
46
|
+
requireInit()
|
|
47
|
+
|
|
48
|
+
const pins = pinsStore.getActive()
|
|
49
|
+
if (pins.length === 0) {
|
|
50
|
+
console.log(chalk.gray('// no active pins'))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.gray('// pins'))
|
|
55
|
+
console.log()
|
|
56
|
+
for (const pin of pins) {
|
|
57
|
+
const label = pin.label ? chalk.white(pin.label) : chalk.gray('(no label)')
|
|
58
|
+
const expiry = pin.expiry !== 'none' ? chalk.gray(` [${pin.expiry}]`) : ''
|
|
59
|
+
console.log(` ${chalk.hex('#aaaaaa')(pin.value.padEnd(6))} ${label}${expiry}`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function cmdPinRotate(pinValue) {
|
|
64
|
+
requireInit()
|
|
65
|
+
|
|
66
|
+
const pin = pinsStore.findByValue(pinValue)
|
|
67
|
+
if (!pin) {
|
|
68
|
+
console.log(chalk.gray(`// pin ${pinValue} not found`))
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const newValue = generatePin()
|
|
73
|
+
pinsStore.rotate(pin.id, newValue)
|
|
74
|
+
|
|
75
|
+
console.log(chalk.gray('// pin rotated'))
|
|
76
|
+
console.log(chalk.gray('old ') + chalk.gray(pinValue))
|
|
77
|
+
console.log(chalk.gray('new ') + chalk.hex('#aaaaaa')(newValue))
|
|
78
|
+
console.log()
|
|
79
|
+
console.log(chalk.gray('// share the new pin with your contact to continue'))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function cmdPinRevoke(pinValue) {
|
|
83
|
+
requireInit()
|
|
84
|
+
|
|
85
|
+
const pin = pinsStore.findByValue(pinValue)
|
|
86
|
+
if (!pin) {
|
|
87
|
+
console.log(chalk.gray(`// pin ${pinValue} not found`))
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pinsStore.revoke(pin.id)
|
|
92
|
+
console.log(chalk.gray(`// pin ${pinValue} revoked`))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function cmdPinInfo(pinValue) {
|
|
96
|
+
requireInit()
|
|
97
|
+
|
|
98
|
+
const pin = pinsStore.findByValue(pinValue)
|
|
99
|
+
if (!pin) {
|
|
100
|
+
console.log(chalk.gray(`// pin ${pinValue} not found`))
|
|
101
|
+
process.exit(1)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(chalk.gray('// pin_info'))
|
|
105
|
+
console.log()
|
|
106
|
+
console.log(chalk.gray('value ') + chalk.hex('#aaaaaa')(pin.value))
|
|
107
|
+
console.log(chalk.gray('label ') + chalk.white(pin.label || '(none)'))
|
|
108
|
+
console.log(chalk.gray('expiry ') + chalk.gray(pin.expiry))
|
|
109
|
+
if (pin.expiresAt) {
|
|
110
|
+
console.log(chalk.gray('expires ') + chalk.gray(new Date(pin.expiresAt).toLocaleString('ja-JP')))
|
|
111
|
+
}
|
|
112
|
+
console.log(chalk.gray('created ') + chalk.gray(new Date(pin.createdAt).toLocaleString('ja-JP')))
|
|
113
|
+
console.log(chalk.gray('active ') + chalk.gray(String(pin.isActive)))
|
|
114
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// stdioモード: エージェントがJSONパイプで0x0を操作する
|
|
2
|
+
// stdin: 1行1コマンド(JSON)
|
|
3
|
+
// stdout: 1行1イベント(JSON) — パーサブルJSON保証
|
|
4
|
+
// stderr: ログのみ
|
|
5
|
+
|
|
6
|
+
import { createInterface } from 'readline'
|
|
7
|
+
import Hyperswarm from 'hyperswarm'
|
|
8
|
+
import { channelSecret } from '../core/channel.js'
|
|
9
|
+
import { createMessage, parseMessage } from '../core/message.js'
|
|
10
|
+
import * as identityStore from '../storage/identity.js'
|
|
11
|
+
import * as messagesStore from '../storage/messages.js'
|
|
12
|
+
import * as pinsStore from '../storage/pins.js'
|
|
13
|
+
|
|
14
|
+
function emit(obj) {
|
|
15
|
+
process.stdout.write(JSON.stringify(obj) + '\n')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function log(msg) {
|
|
19
|
+
process.stderr.write(msg + '\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function cmdPipe(theirNumber, pin) {
|
|
23
|
+
const identity = identityStore.load()
|
|
24
|
+
if (!identity) {
|
|
25
|
+
emit({ type: 'error', code: 'NOT_INITIALIZED', message: 'run: 0x0 init' })
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const swarm = new Hyperswarm()
|
|
30
|
+
const topic = channelSecret(theirNumber, pin)
|
|
31
|
+
let activeConn = null
|
|
32
|
+
|
|
33
|
+
log(`// connecting to ${theirNumber} via pin ${pin}...`)
|
|
34
|
+
|
|
35
|
+
swarm.join(topic, { server: true, client: true })
|
|
36
|
+
|
|
37
|
+
swarm.on('connection', (conn) => {
|
|
38
|
+
activeConn = conn
|
|
39
|
+
emit({ type: 'connected', peer: theirNumber, pin })
|
|
40
|
+
|
|
41
|
+
conn.on('error', () => {})
|
|
42
|
+
conn.on('data', (data) => {
|
|
43
|
+
const msg = parseMessage(data)
|
|
44
|
+
if (!msg || msg.type !== 'message') return
|
|
45
|
+
|
|
46
|
+
const localPin = pinsStore.findByValue(pin)
|
|
47
|
+
if (localPin) {
|
|
48
|
+
messagesStore.append(localPin.value, {
|
|
49
|
+
from: theirNumber, content: msg.content, isMine: false
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
emit({
|
|
54
|
+
type: 'message',
|
|
55
|
+
from: theirNumber,
|
|
56
|
+
content: msg.content,
|
|
57
|
+
timestamp: Date.now()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
conn.on('close', () => {
|
|
62
|
+
activeConn = null
|
|
63
|
+
emit({ type: 'disconnected', peer: theirNumber })
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// stdin からコマンドを受け取る
|
|
68
|
+
const rl = createInterface({ input: process.stdin })
|
|
69
|
+
|
|
70
|
+
rl.on('line', (line) => {
|
|
71
|
+
const trimmed = line.trim()
|
|
72
|
+
if (!trimmed) return
|
|
73
|
+
|
|
74
|
+
let cmd
|
|
75
|
+
try { cmd = JSON.parse(trimmed) } catch {
|
|
76
|
+
emit({ type: 'error', code: 'INVALID_JSON', message: 'stdin must be JSON' })
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
switch (cmd.type) {
|
|
81
|
+
case 'message': {
|
|
82
|
+
if (!activeConn) {
|
|
83
|
+
emit({ type: 'error', code: 'PEER_OFFLINE', message: 'not connected' })
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
activeConn.write(createMessage(cmd.content))
|
|
87
|
+
|
|
88
|
+
const localPin = pinsStore.findByValue(pin)
|
|
89
|
+
if (localPin) {
|
|
90
|
+
messagesStore.append(localPin.value, {
|
|
91
|
+
from: identity.number, content: cmd.content, isMine: true
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
emit({ type: 'sent', content: cmd.content, timestamp: Date.now() })
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case 'ping':
|
|
100
|
+
emit({ type: 'pong', timestamp: Date.now() })
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
case 'disconnect':
|
|
104
|
+
rl.close()
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
emit({ type: 'error', code: 'UNKNOWN_CMD', message: `unknown type: ${cmd.type}` })
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
rl.on('close', async () => {
|
|
113
|
+
await swarm.destroy()
|
|
114
|
+
process.exit(0)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
process.on('SIGINT', async () => {
|
|
118
|
+
await swarm.destroy()
|
|
119
|
+
process.exit(0)
|
|
120
|
+
})
|
|
121
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import qrcode from 'qrcode-terminal'
|
|
3
|
+
import { buildUri } from '../core/uri.js'
|
|
4
|
+
import * as identityStore from '../storage/identity.js'
|
|
5
|
+
import * as pinsStore from '../storage/pins.js'
|
|
6
|
+
|
|
7
|
+
function log(msg) {
|
|
8
|
+
console.log(chalk.gray(msg))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function cmdQr(pin) {
|
|
12
|
+
const identity = identityStore.load()
|
|
13
|
+
if (!identity) {
|
|
14
|
+
log('// not initialized. run: 0x0 init')
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pinEntry = pinsStore.findByValue(pin)
|
|
19
|
+
if (!pinEntry) {
|
|
20
|
+
log(`// pin not found: ${pin}`)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const uri = buildUri(identity.number, pin)
|
|
25
|
+
|
|
26
|
+
console.log()
|
|
27
|
+
log(`// ${uri}`)
|
|
28
|
+
console.log()
|
|
29
|
+
|
|
30
|
+
qrcode.generate(uri, { small: true })
|
|
31
|
+
|
|
32
|
+
const label = pinEntry.label ? ` · label: ${pinEntry.label}` : ''
|
|
33
|
+
console.log()
|
|
34
|
+
log(`// scan to connect${label}`)
|
|
35
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import * as identityStore from '../storage/identity.js'
|
|
3
|
+
import * as pinsStore from '../storage/pins.js'
|
|
4
|
+
import * as messagesStore from '../storage/messages.js'
|
|
5
|
+
|
|
6
|
+
function printMessages(msgs) {
|
|
7
|
+
if (msgs.length === 0) {
|
|
8
|
+
console.log(chalk.gray(' (no messages)'))
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
for (const msg of msgs) {
|
|
12
|
+
const time = new Date(msg.timestamp).toLocaleTimeString('ja-JP', {
|
|
13
|
+
hour: '2-digit', minute: '2-digit'
|
|
14
|
+
})
|
|
15
|
+
if (msg.isMine) {
|
|
16
|
+
console.log(chalk.gray(`[${time}]`) + chalk.gray(' you: ') + chalk.white(msg.content))
|
|
17
|
+
} else {
|
|
18
|
+
const from = msg.from ? msg.from.slice(0, 14) + '…' : 'them'
|
|
19
|
+
console.log(chalk.gray(`[${time}]`) + chalk.gray(` ${from}: `) + chalk.hex('#aaaaaa')(msg.content))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function cmdRead(pinValue, { json = false } = {}) {
|
|
25
|
+
const identity = identityStore.load()
|
|
26
|
+
if (!identity) {
|
|
27
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pin = pinsStore.findByValue(pinValue)
|
|
32
|
+
if (!pin) {
|
|
33
|
+
console.log(chalk.gray(`// pin ${pinValue} not found or inactive`))
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const msgs = messagesStore.list(pin.value)
|
|
38
|
+
|
|
39
|
+
if (json) {
|
|
40
|
+
console.log(JSON.stringify(msgs, null, 2))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const label = pin.label ? ` label: ${pin.label}` : ''
|
|
45
|
+
console.log(chalk.gray(`// pin: ${pin.value}${label}`))
|
|
46
|
+
console.log(chalk.gray('─'.repeat(50)))
|
|
47
|
+
printMessages(msgs)
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { generateNumber } from '../core/identity.js'
|
|
3
|
+
import * as identityStore from '../storage/identity.js'
|
|
4
|
+
import * as pinsStore from '../storage/pins.js'
|
|
5
|
+
|
|
6
|
+
export async function cmdRenew() {
|
|
7
|
+
const identity = identityStore.load()
|
|
8
|
+
if (!identity) {
|
|
9
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 全PINを無効化
|
|
14
|
+
const pins = pinsStore.getActive()
|
|
15
|
+
for (const pin of pins) {
|
|
16
|
+
pinsStore.revoke(pin.id)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const newNumber = generateNumber()
|
|
20
|
+
identityStore.save({ ...identity, number: newNumber, renewedAt: Date.now() })
|
|
21
|
+
|
|
22
|
+
console.log(chalk.gray('// renewing number...'))
|
|
23
|
+
console.log(chalk.gray(`// ${pins.length} pin(s) revoked`))
|
|
24
|
+
console.log()
|
|
25
|
+
console.log(chalk.gray('new number ') + chalk.white(newNumber))
|
|
26
|
+
console.log()
|
|
27
|
+
console.log(chalk.gray('// all previous connections are now disconnected'))
|
|
28
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import Hyperswarm from 'hyperswarm'
|
|
4
|
+
import { channelSecret } from '../core/channel.js'
|
|
5
|
+
import { createMessage } from '../core/message.js'
|
|
6
|
+
import * as identityStore from '../storage/identity.js'
|
|
7
|
+
import * as messagesStore from '../storage/messages.js'
|
|
8
|
+
import * as pinsStore from '../storage/pins.js'
|
|
9
|
+
|
|
10
|
+
const TTL = 10_000 // 10秒で接続タイムアウト
|
|
11
|
+
|
|
12
|
+
export async function cmdSend(theirNumber, pin, content) {
|
|
13
|
+
const identity = identityStore.load()
|
|
14
|
+
if (!identity) {
|
|
15
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const spinner = ora({
|
|
20
|
+
text: chalk.gray(`connecting to ${theirNumber}...`),
|
|
21
|
+
color: 'white'
|
|
22
|
+
}).start()
|
|
23
|
+
|
|
24
|
+
const swarm = new Hyperswarm()
|
|
25
|
+
const topic = channelSecret(theirNumber, pin)
|
|
26
|
+
let sent = false
|
|
27
|
+
|
|
28
|
+
swarm.join(topic, { server: false, client: true })
|
|
29
|
+
|
|
30
|
+
swarm.on('connection', async (conn) => {
|
|
31
|
+
spinner.stop()
|
|
32
|
+
conn.on('error', () => {})
|
|
33
|
+
|
|
34
|
+
conn.write(createMessage(content))
|
|
35
|
+
|
|
36
|
+
// ローカルPINがあればメッセージを保存
|
|
37
|
+
const localPin = pinsStore.findByValue(pin)
|
|
38
|
+
if (localPin) {
|
|
39
|
+
messagesStore.append(localPin.value, {
|
|
40
|
+
from: identity.number,
|
|
41
|
+
content,
|
|
42
|
+
isMine: true
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(chalk.gray('[sent]'))
|
|
47
|
+
sent = true
|
|
48
|
+
await swarm.destroy()
|
|
49
|
+
process.exit(0)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
setTimeout(async () => {
|
|
53
|
+
if (!sent) {
|
|
54
|
+
spinner.stop()
|
|
55
|
+
console.log(chalk.gray('// peer offline'))
|
|
56
|
+
console.log(chalk.gray('// message will be delivered when they come online (TTL: 72h)'))
|
|
57
|
+
await swarm.destroy()
|
|
58
|
+
process.exit(0)
|
|
59
|
+
}
|
|
60
|
+
}, TTL)
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import open from 'open'
|
|
3
|
+
import { startWebServer, getLanIps } from '../web/server.js'
|
|
4
|
+
import * as identityStore from '../storage/identity.js'
|
|
5
|
+
|
|
6
|
+
export async function cmdWeb({ port = 3000, noOpen = false } = {}) {
|
|
7
|
+
const identity = identityStore.load()
|
|
8
|
+
if (!identity) {
|
|
9
|
+
console.log(chalk.gray('// not initialized. run: 0x0 init'))
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log(chalk.gray('// starting 0x0 web...'))
|
|
14
|
+
|
|
15
|
+
const { port: actualPort } = await startWebServer(port)
|
|
16
|
+
const localhost = `http://localhost:${actualPort}`
|
|
17
|
+
|
|
18
|
+
console.log()
|
|
19
|
+
console.log(chalk.gray('// local'))
|
|
20
|
+
console.log(chalk.white(` ${localhost}`))
|
|
21
|
+
|
|
22
|
+
const lanIps = getLanIps()
|
|
23
|
+
if (lanIps.length > 0) {
|
|
24
|
+
console.log()
|
|
25
|
+
console.log(chalk.gray('// mobile (same wifi)'))
|
|
26
|
+
for (const ip of lanIps) {
|
|
27
|
+
console.log(chalk.white(` http://${ip}:${actualPort}`))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log()
|
|
32
|
+
console.log(chalk.gray('// ctrl+c to stop'))
|
|
33
|
+
|
|
34
|
+
if (!noOpen) {
|
|
35
|
+
await open(localhost)
|
|
36
|
+
}
|
|
37
|
+
}
|