mirror of
https://github.com/hanzoskill/botlink.git
synced 2026-10-30 21:01:49 +00:00
223 lines
5.6 KiB
JavaScript
223 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ClawdLink Friends Manager
|
|
* Add, list, and manage friends
|
|
*/
|
|
|
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
import { homedir } from 'os';
|
|
import { join } from 'path';
|
|
import crypto from '../lib/crypto.js';
|
|
|
|
const DATA_DIR = join(homedir(), '.bot', 'clawdlink');
|
|
const IDENTITY_FILE = join(DATA_DIR, 'identity.json');
|
|
const FRIENDS_FILE = join(DATA_DIR, 'friends.json');
|
|
const CONFIG_FILE = join(DATA_DIR, 'config.json');
|
|
|
|
function loadIdentity() {
|
|
if (!existsSync(IDENTITY_FILE)) {
|
|
throw new Error('No identity found. Run setup first.');
|
|
}
|
|
return JSON.parse(readFileSync(IDENTITY_FILE, 'utf8'));
|
|
}
|
|
|
|
function loadFriends() {
|
|
if (!existsSync(FRIENDS_FILE)) {
|
|
return { friends: [] };
|
|
}
|
|
return JSON.parse(readFileSync(FRIENDS_FILE, 'utf8'));
|
|
}
|
|
|
|
function saveFriends(data) {
|
|
writeFileSync(FRIENDS_FILE, JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
function loadConfig() {
|
|
if (!existsSync(CONFIG_FILE)) {
|
|
return { displayName: 'ClawdLink User' };
|
|
}
|
|
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
}
|
|
|
|
/**
|
|
* Parse a friend link
|
|
* Format: clawdlink://<relay-url>/add?key=ed25519:<pubkey>&name=<name>
|
|
*/
|
|
function parseFriendLink(link) {
|
|
try {
|
|
// Handle both clawdlink:// and https:// formats
|
|
const url = new URL(link.replace('clawdlink://', 'https://'));
|
|
const params = new URLSearchParams(url.search);
|
|
|
|
let key = params.get('key') || '';
|
|
const name = params.get('name') || 'Unknown';
|
|
|
|
// Remove ed25519: prefix if present
|
|
if (key.startsWith('ed25519:')) {
|
|
key = key.slice(8);
|
|
}
|
|
|
|
if (!key) {
|
|
throw new Error('No public key found in link');
|
|
}
|
|
|
|
return { publicKey: key, displayName: decodeURIComponent(name) };
|
|
} catch (err) {
|
|
throw new Error(`Invalid friend link: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate your friend link
|
|
*/
|
|
function generateFriendLink(identity, config) {
|
|
const params = new URLSearchParams({
|
|
key: `ed25519:${identity.publicKey}`,
|
|
name: config.displayName
|
|
});
|
|
return `clawdlink://relay.clawdlink.bot/add?${params.toString()}`;
|
|
}
|
|
|
|
/**
|
|
* Add a friend
|
|
*/
|
|
function addFriend(link) {
|
|
const identity = loadIdentity();
|
|
const data = loadFriends();
|
|
|
|
// Parse the friend link
|
|
const { publicKey, displayName } = parseFriendLink(link);
|
|
|
|
// Check if already added
|
|
const existing = data.friends.find(f => f.publicKey === publicKey);
|
|
if (existing) {
|
|
console.log(`✓ ${displayName} is already your friend.`);
|
|
return;
|
|
}
|
|
|
|
// Derive shared secret for encryption
|
|
const theirX25519Public = crypto.ed25519ToX25519({ secretKey: identity.secretKey, publicKey }).publicKey;
|
|
// For now, use the friend's public key directly (they'll derive same secret)
|
|
// In a full implementation, we'd do proper X25519 key exchange
|
|
const sharedSecret = crypto.deriveSharedSecret(identity.x25519SecretKey, publicKey);
|
|
|
|
// Add friend
|
|
const friend = {
|
|
displayName,
|
|
publicKey,
|
|
sharedSecret: Buffer.from(sharedSecret).toString('base64'),
|
|
addedAt: new Date().toISOString(),
|
|
status: 'connected' // With relay, we're always "connected"
|
|
};
|
|
|
|
data.friends.push(friend);
|
|
saveFriends(data);
|
|
|
|
console.log(`✓ Added friend: ${displayName}`);
|
|
console.log(` Public Key: ${publicKey.slice(0, 20)}...`);
|
|
}
|
|
|
|
/**
|
|
* List friends
|
|
*/
|
|
function listFriends() {
|
|
const data = loadFriends();
|
|
|
|
if (data.friends.length === 0) {
|
|
console.log('No friends yet.');
|
|
console.log('');
|
|
console.log('To add a friend, share your friend link with them:');
|
|
showLink();
|
|
return;
|
|
}
|
|
|
|
console.log(`Friends (${data.friends.length}):`);
|
|
console.log('');
|
|
|
|
for (const friend of data.friends) {
|
|
console.log(` • ${friend.displayName}`);
|
|
console.log(` Key: ${friend.publicKey.slice(0, 20)}...`);
|
|
console.log(` Status: ${friend.status}`);
|
|
console.log(` Added: ${friend.addedAt}`);
|
|
console.log('');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a friend
|
|
*/
|
|
function removeFriend(nameOrKey) {
|
|
const data = loadFriends();
|
|
const query = nameOrKey.toLowerCase();
|
|
|
|
const index = data.friends.findIndex(f =>
|
|
f.displayName?.toLowerCase().includes(query) ||
|
|
f.publicKey?.toLowerCase().includes(query)
|
|
);
|
|
|
|
if (index === -1) {
|
|
console.error(`✗ Friend not found: ${nameOrKey}`);
|
|
return;
|
|
}
|
|
|
|
const friend = data.friends[index];
|
|
data.friends.splice(index, 1);
|
|
saveFriends(data);
|
|
|
|
console.log(`✓ Removed friend: ${friend.displayName}`);
|
|
}
|
|
|
|
/**
|
|
* Show your friend link
|
|
*/
|
|
function showLink() {
|
|
const identity = loadIdentity();
|
|
const config = loadConfig();
|
|
const link = generateFriendLink(identity, config);
|
|
|
|
console.log('Your friend link:');
|
|
console.log('');
|
|
console.log(` ${link}`);
|
|
console.log('');
|
|
console.log('Share this with friends so they can add you.');
|
|
}
|
|
|
|
// Main
|
|
const args = process.argv.slice(2);
|
|
const command = args[0];
|
|
|
|
switch (command) {
|
|
case 'add':
|
|
if (!args[1]) {
|
|
console.error('Usage: friends.js add <friend-link>');
|
|
process.exit(1);
|
|
}
|
|
addFriend(args[1]);
|
|
break;
|
|
|
|
case 'list':
|
|
listFriends();
|
|
break;
|
|
|
|
case 'remove':
|
|
if (!args[1]) {
|
|
console.error('Usage: friends.js remove <name>');
|
|
process.exit(1);
|
|
}
|
|
removeFriend(args[1]);
|
|
break;
|
|
|
|
case 'link':
|
|
showLink();
|
|
break;
|
|
|
|
default:
|
|
console.log('ClawdLink Friends Manager');
|
|
console.log('');
|
|
console.log('Commands:');
|
|
console.log(' add <link> Add a friend from their link');
|
|
console.log(' list List all friends');
|
|
console.log(' remove <name> Remove a friend');
|
|
console.log(' link Show your friend link');
|
|
}
|