415 lines
11 KiB
Go
415 lines
11 KiB
Go
// pkg/mycelium_client/cmd/main.go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/freeflowuniverse/heroagent/pkg/mycelium_client"
|
|
)
|
|
|
|
type config struct {
|
|
baseURL string
|
|
command string
|
|
peerEndpoint string
|
|
message string
|
|
destination string
|
|
topic string
|
|
timeout int
|
|
wait bool
|
|
replyTimeout int
|
|
messageID string
|
|
outputJSON bool
|
|
}
|
|
|
|
// Commands
|
|
const (
|
|
cmdInfo = "info"
|
|
cmdPeers = "peers"
|
|
cmdAddPeer = "add-peer"
|
|
cmdDelPeer = "del-peer"
|
|
cmdSend = "send"
|
|
cmdReceive = "receive"
|
|
cmdReply = "reply"
|
|
cmdStatus = "status"
|
|
cmdRoutes = "routes"
|
|
)
|
|
|
|
func main() {
|
|
// Create config with default values
|
|
cfg := config{
|
|
baseURL: fmt.Sprintf("http://localhost:%d", mycelium_client.DefaultAPIPort),
|
|
timeout: 30,
|
|
replyTimeout: mycelium_client.DefaultReplyTimeout,
|
|
}
|
|
|
|
// Parse command line flags
|
|
flag.StringVar(&cfg.baseURL, "api", cfg.baseURL, "Mycelium API URL")
|
|
flag.IntVar(&cfg.timeout, "timeout", cfg.timeout, "Client timeout in seconds")
|
|
flag.BoolVar(&cfg.outputJSON, "json", false, "Output in JSON format")
|
|
flag.Parse()
|
|
|
|
// Get the command
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
cfg.command = args[0]
|
|
args = args[1:]
|
|
|
|
// Create client
|
|
client := mycelium_client.NewClient(cfg.baseURL)
|
|
client.SetTimeout(time.Duration(cfg.timeout) * time.Second)
|
|
|
|
// Create context with cancellation for graceful shutdowns
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Set up signal handling
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
<-sigCh
|
|
fmt.Println("\nReceived interrupt signal, shutting down...")
|
|
cancel()
|
|
}()
|
|
|
|
// Execute command
|
|
var err error
|
|
switch cfg.command {
|
|
case cmdInfo:
|
|
err = showNodeInfo(ctx, client)
|
|
|
|
case cmdPeers:
|
|
err = listPeers(ctx, client, cfg.outputJSON)
|
|
|
|
case cmdAddPeer:
|
|
if len(args) < 1 {
|
|
fmt.Println("Missing peer endpoint argument")
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
cfg.peerEndpoint = args[0]
|
|
err = addPeer(ctx, client, cfg.peerEndpoint)
|
|
|
|
case cmdDelPeer:
|
|
if len(args) < 1 {
|
|
fmt.Println("Missing peer endpoint argument")
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
cfg.peerEndpoint = args[0]
|
|
err = removePeer(ctx, client, cfg.peerEndpoint)
|
|
|
|
case cmdSend:
|
|
parseMessageArgs(&cfg, args)
|
|
err = sendMessage(ctx, client, cfg)
|
|
|
|
case cmdReceive:
|
|
parseReceiveArgs(&cfg, args)
|
|
err = receiveMessage(ctx, client, cfg)
|
|
|
|
case cmdReply:
|
|
parseReplyArgs(&cfg, args)
|
|
err = replyToMessage(ctx, client, cfg)
|
|
|
|
case cmdStatus:
|
|
if len(args) < 1 {
|
|
fmt.Println("Missing message ID argument")
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
cfg.messageID = args[0]
|
|
err = getMessageStatus(ctx, client, cfg.messageID)
|
|
|
|
case cmdRoutes:
|
|
var routeType string
|
|
if len(args) > 0 {
|
|
routeType = args[0]
|
|
}
|
|
err = listRoutes(ctx, client, routeType, cfg.outputJSON)
|
|
|
|
default:
|
|
fmt.Printf("Unknown command: %s\n", cfg.command)
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func printUsage() {
|
|
fmt.Println("Usage: mycelium-client [flags] COMMAND [args...]")
|
|
fmt.Println("\nFlags:")
|
|
flag.PrintDefaults()
|
|
fmt.Println("\nCommands:")
|
|
fmt.Println(" info Get node information")
|
|
fmt.Println(" peers List connected peers")
|
|
fmt.Println(" add-peer ENDPOINT Add a new peer")
|
|
fmt.Println(" del-peer ENDPOINT Remove a peer")
|
|
fmt.Println(" send [--pk=PK|--ip=IP] [--topic=TOPIC] [--wait] [--reply-timeout=N] MESSAGE")
|
|
fmt.Println(" Send a message to a destination")
|
|
fmt.Println(" receive [--topic=TOPIC] [--timeout=N]")
|
|
fmt.Println(" Receive a message")
|
|
fmt.Println(" reply ID [--topic=TOPIC] MESSAGE")
|
|
fmt.Println(" Reply to a message")
|
|
fmt.Println(" status ID Get status of a sent message")
|
|
fmt.Println(" routes [selected|fallback] List routes (default: selected)")
|
|
}
|
|
|
|
func parseMessageArgs(cfg *config, args []string) {
|
|
// Create a temporary flag set
|
|
fs := flag.NewFlagSet("send", flag.ExitOnError)
|
|
fs.StringVar(&cfg.destination, "pk", "", "Destination public key (hex encoded)")
|
|
fs.StringVar(&cfg.destination, "ip", "", "Destination IP address")
|
|
fs.StringVar(&cfg.topic, "topic", "", "Message topic")
|
|
fs.BoolVar(&cfg.wait, "wait", false, "Wait for reply")
|
|
fs.IntVar(&cfg.replyTimeout, "reply-timeout", cfg.replyTimeout, "Reply timeout in seconds")
|
|
|
|
// Parse args
|
|
fs.Parse(args)
|
|
|
|
// Remaining args are the message
|
|
remainingArgs := fs.Args()
|
|
if len(remainingArgs) == 0 {
|
|
fmt.Println("Missing message content")
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
cfg.message = strings.Join(remainingArgs, " ")
|
|
}
|
|
|
|
func parseReceiveArgs(cfg *config, args []string) {
|
|
// Create a temporary flag set
|
|
fs := flag.NewFlagSet("receive", flag.ExitOnError)
|
|
fs.StringVar(&cfg.topic, "topic", "", "Message topic filter")
|
|
fs.IntVar(&cfg.timeout, "timeout", 10, "Receive timeout in seconds")
|
|
|
|
// Parse args
|
|
fs.Parse(args)
|
|
}
|
|
|
|
func parseReplyArgs(cfg *config, args []string) {
|
|
if len(args) < 1 {
|
|
fmt.Println("Missing message ID argument")
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
cfg.messageID = args[0]
|
|
args = args[1:]
|
|
|
|
// Create a temporary flag set
|
|
fs := flag.NewFlagSet("reply", flag.ExitOnError)
|
|
fs.StringVar(&cfg.topic, "topic", "", "Message topic")
|
|
|
|
// Parse args
|
|
fs.Parse(args)
|
|
|
|
// Remaining args are the message
|
|
remainingArgs := fs.Args()
|
|
if len(remainingArgs) == 0 {
|
|
fmt.Println("Missing reply message content")
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
cfg.message = strings.Join(remainingArgs, " ")
|
|
}
|
|
|
|
func showNodeInfo(ctx context.Context, client *mycelium_client.MyceliumClient) error {
|
|
info, err := client.GetNodeInfo(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("Node Information:")
|
|
fmt.Printf(" Subnet: %s\n", info.NodeSubnet)
|
|
return nil
|
|
}
|
|
|
|
func listPeers(ctx context.Context, client *mycelium_client.MyceliumClient, jsonOutput bool) error {
|
|
peers, err := client.ListPeers(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if jsonOutput {
|
|
// TODO: Output JSON
|
|
fmt.Printf("Found %d peers\n", len(peers))
|
|
} else {
|
|
fmt.Printf("Connected Peers (%d):\n", len(peers))
|
|
if len(peers) == 0 {
|
|
fmt.Println(" No peers connected")
|
|
return nil
|
|
}
|
|
|
|
for i, peer := range peers {
|
|
fmt.Printf(" %d. %s://%s\n", i+1, peer.Endpoint.Proto, peer.Endpoint.SocketAddr)
|
|
fmt.Printf(" Type: %s, State: %s\n", peer.Type, peer.ConnectionState)
|
|
if peer.TxBytes > 0 || peer.RxBytes > 0 {
|
|
fmt.Printf(" TX: %d bytes, RX: %d bytes\n", peer.TxBytes, peer.RxBytes)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addPeer(ctx context.Context, client *mycelium_client.MyceliumClient, endpoint string) error {
|
|
if err := client.AddPeer(ctx, endpoint); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Peer added: %s\n", endpoint)
|
|
return nil
|
|
}
|
|
|
|
func removePeer(ctx context.Context, client *mycelium_client.MyceliumClient, endpoint string) error {
|
|
if err := client.RemovePeer(ctx, endpoint); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Peer removed: %s\n", endpoint)
|
|
return nil
|
|
}
|
|
|
|
func sendMessage(ctx context.Context, client *mycelium_client.MyceliumClient, cfg config) error {
|
|
var dst mycelium_client.MessageDestination
|
|
|
|
if cfg.destination == "" {
|
|
return fmt.Errorf("destination is required (--pk or --ip)")
|
|
}
|
|
|
|
// Determine destination type
|
|
if strings.HasPrefix(cfg.destination, "--pk=") {
|
|
dst.PK = strings.TrimPrefix(cfg.destination, "--pk=")
|
|
} else if strings.HasPrefix(cfg.destination, "--ip=") {
|
|
dst.IP = strings.TrimPrefix(cfg.destination, "--ip=")
|
|
} else {
|
|
// Try to guess format
|
|
if strings.Contains(cfg.destination, ":") {
|
|
dst.IP = cfg.destination
|
|
} else {
|
|
dst.PK = cfg.destination
|
|
}
|
|
}
|
|
|
|
// Send message
|
|
payload := []byte(cfg.message)
|
|
reply, id, err := client.SendMessage(ctx, dst, payload, cfg.topic, cfg.wait, cfg.replyTimeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if reply != nil {
|
|
fmt.Println("Received reply:")
|
|
printMessage(reply)
|
|
} else {
|
|
fmt.Printf("Message sent successfully. ID: %s\n", id)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func receiveMessage(ctx context.Context, client *mycelium_client.MyceliumClient, cfg config) error {
|
|
fmt.Printf("Waiting for message (timeout: %d seconds)...\n", cfg.timeout)
|
|
msg, err := client.ReceiveMessage(ctx, cfg.timeout, cfg.topic, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if msg == nil {
|
|
fmt.Println("No message received within timeout")
|
|
return nil
|
|
}
|
|
|
|
fmt.Println("Message received:")
|
|
printMessage(msg)
|
|
return nil
|
|
}
|
|
|
|
func replyToMessage(ctx context.Context, client *mycelium_client.MyceliumClient, cfg config) error {
|
|
if err := client.ReplyToMessage(ctx, cfg.messageID, []byte(cfg.message), cfg.topic); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Reply sent to message ID: %s\n", cfg.messageID)
|
|
return nil
|
|
}
|
|
|
|
func getMessageStatus(ctx context.Context, client *mycelium_client.MyceliumClient, messageID string) error {
|
|
status, err := client.GetMessageStatus(ctx, messageID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Message Status (ID: %s):\n", messageID)
|
|
for k, v := range status {
|
|
fmt.Printf(" %s: %v\n", k, v)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func listRoutes(ctx context.Context, client *mycelium_client.MyceliumClient, routeType string, jsonOutput bool) error {
|
|
var routes []mycelium_client.Route
|
|
var err error
|
|
|
|
// Default to selected routes
|
|
if routeType == "" || routeType == "selected" {
|
|
routes, err = client.ListSelectedRoutes(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Selected Routes (%d):\n", len(routes))
|
|
} else if routeType == "fallback" {
|
|
routes, err = client.ListFallbackRoutes(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Fallback Routes (%d):\n", len(routes))
|
|
} else {
|
|
return fmt.Errorf("unknown route type: %s (use 'selected' or 'fallback')", routeType)
|
|
}
|
|
|
|
if jsonOutput {
|
|
// TODO: Output JSON
|
|
fmt.Printf("Found %d routes\n", len(routes))
|
|
} else {
|
|
if len(routes) == 0 {
|
|
fmt.Println(" No routes found")
|
|
return nil
|
|
}
|
|
|
|
for i, route := range routes {
|
|
fmt.Printf(" %d. Subnet: %s\n", i+1, route.Subnet)
|
|
fmt.Printf(" Next Hop: %s\n", route.NextHop)
|
|
fmt.Printf(" Metric: %v, Sequence: %d\n", route.Metric, route.Seqno)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func printMessage(msg *mycelium_client.InboundMessage) {
|
|
payload, err := msg.Decode()
|
|
fmt.Printf(" ID: %s\n", msg.ID)
|
|
fmt.Printf(" From: %s (IP: %s)\n", msg.SrcPK, msg.SrcIP)
|
|
fmt.Printf(" To: %s (IP: %s)\n", msg.DstPK, msg.DstIP)
|
|
if msg.Topic != "" {
|
|
fmt.Printf(" Topic: %s\n", msg.Topic)
|
|
}
|
|
if err != nil {
|
|
fmt.Printf(" Payload (base64): %s\n", msg.Payload)
|
|
fmt.Printf(" Error decoding payload: %v\n", err)
|
|
} else {
|
|
fmt.Printf(" Payload: %s\n", string(payload))
|
|
}
|
|
}
|