heroagent/pkg/clients/ mycelium/cmd/main.go
2025-04-23 04:18:28 +02:00

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))
}
}