a cabal implementation in erlang
#!/usr/bin/env escript
% SPDX-FileCopyrightText: 2023 Henry Bubert
%
% SPDX-License-Identifier: LGPL-2.1-or-later

%%! -pa _build/default/lib/*/ebin
%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ft=erlang ts=4 sw=4 et

%% Headless Cable Peer
%%
%% A simple headless peer that can join channels and connect to other peers.
%% Uses the default Cable PSK (0808...08)

-mode(compile).

main(Args) ->
    % Parse command line arguments
    case parse_args(Args) of
        {ok, Config} ->
            case maps:get(help, Config, false) of
                true ->
                    print_help(),
                    halt(0);
                false ->
                    run_peer(Config)
            end;
        {error, Reason} ->
            io:format("Error: ~s~n~n", [Reason]),
            print_help(),
            halt(1)
    end.

run_peer(Config) ->
    io:format("~n=== Starting Cable Peer ===~n~n"),

    % Start required applications
    ok = application:ensure_started(crypto),
    ok = application:ensure_started(asn1),
    ok = application:ensure_started(public_key),
    ok = application:ensure_started(ssl),

    % Extract configuration
    Storage = maps:get(storage, Config, default_storage()),
    Port = maps:get(port, Config, 3113),
    Channels = maps:get(channels, Config, []),
    Peers = maps:get(peers, Config, []),
    Nick = maps:get(nick, Config, undefined),

    io:format("Configuration:~n"),
    io:format("  Storage:  ~s~n", [Storage]),
    io:format("  Port:     ~p~n", [Port]),
    io:format("  Channels: ~p~n", [Channels]),
    io:format("  Peers:    ~p~n", [Peers]),
    case Nick of
        undefined -> ok;
        _ -> io:format("  Nickname: ~s~n", [Nick])
    end,
    io:format("~n"),

    % Start the peer
    io:format("Starting peer...~n"),
    ListenAddr = ["0.0.0.0", Port],
    {ok, PeerPid} = peer:start_link([
        {listener, ListenAddr},
        {storage, Storage}
    ]),

    % Get peer info
    {ok, Addr} = peer:node_addr(PeerPid),
    {ok, PubKey} = peer:node_public_key(PeerPid),
    io:format("Peer started!~n"),
    io:format("  Address:    ~p~n", [Addr]),
    io:format("  Public Key: ~s~n", [hex:bin_to_hexstr(PubKey)]),
    io:format("~n"),

    % Set nickname if provided
    case Nick of
        undefined ->
            ok;
        _ ->
            io:format("Setting nickname to '~s'...~n", [Nick]),
            ok = peer:set_nick(PeerPid, Nick)
    end,

    % Join channels
    lists:foreach(
        fun(Chan) ->
            io:format("Joining channel: ~s~n", [Chan]),
            peer:join(PeerPid, Chan),
            timer:sleep(100)  % Small delay between joins
        end,
        Channels
    ),

    % Wait a bit for channels to be joined
    case length(Channels) of
        0 -> ok;
        _ ->
            timer:sleep(500),
            io:format("~n")
    end,

    % Connect to peers
    lists:foreach(
        fun(PeerAddr) ->
            io:format("Connecting to peer: ~s~n", [PeerAddr]),
            peer:dial(PeerPid, PeerAddr),
            timer:sleep(100)  % Small delay between connections
        end,
        Peers
    ),

    case length(Peers) of
        0 -> ok;
        _ ->
            timer:sleep(500),
            io:format("~n")
    end,

    io:format("~n=== Peer is running ===~n"),
    io:format("Press Ctrl+C to stop~n~n"),
    io:format("Note: Using default Cable PSK (cabal key)~n"),
    io:format("      0808080808080808080808080808080808080808080808080808080808080808~n~n"),

    % Keep running until interrupted
    receive
        stop -> ok
    end.

parse_args(Args) ->
    parse_args(Args, #{}).

parse_args([], Acc) ->
    {ok, Acc};
parse_args(["--help" | _], _Acc) ->
    {ok, #{help => true}};
parse_args(["-h" | _], _Acc) ->
    {ok, #{help => true}};
parse_args(["--storage", Path | Rest], Acc) ->
    parse_args(Rest, Acc#{storage => Path});
parse_args(["--port", PortStr | Rest], Acc) ->
    case string:to_integer(PortStr) of
        {Port, ""} when Port > 0, Port =< 65535 ->
            parse_args(Rest, Acc#{port => Port});
        _ ->
            {error, io_lib:format("Invalid port: ~s", [PortStr])}
    end;
parse_args(["--channels", ChansStr | Rest], Acc) ->
    Channels = string:split(ChansStr, ",", all),
    parse_args(Rest, Acc#{channels => Channels});
parse_args(["--peers", PeersStr | Rest], Acc) ->
    Peers = string:split(PeersStr, ",", all),
    parse_args(Rest, Acc#{peers => Peers});
parse_args(["--nick", Nick | Rest], Acc) ->
    parse_args(Rest, Acc#{nick => Nick});
parse_args([Unknown | _], _Acc) ->
    {error, io_lib:format("Unknown option: ~s", [Unknown])}.

default_storage() ->
    Home = os:getenv("HOME"),
    filename:join(Home, ".caberl").

print_help() ->
    io:format("~nCable Peer~n"),
    io:format("==========~n~n"),
    io:format("Usage: caberl.escript [OPTIONS]~n~n"),
    io:format("Options:~n"),
    io:format("  --storage PATH          Storage directory for database and keys~n"),
    io:format("                          (default: ~~/.caberl)~n"),
    io:format("  --port PORT             Port to listen on (default: 3113)~n"),
    io:format("  --channels CHAN1,CHAN2  Comma-separated list of channels to join~n"),
    io:format("  --peers ADDR1,ADDR2     Comma-separated list of peers (host:port)~n"),
    io:format("  --nick NICKNAME         Set nickname on startup~n"),
    io:format("  -h, --help              Show this help message~n"),
    io:format("~n"),
    io:format("Examples:~n"),
    io:format("  # Start peer on default port, join 'general' channel~n"),
    io:format("  ./caberl.escript --channels general --nick 'Bot'~n~n"),
    io:format("  # Join multiple channels and connect to a peer~n"),
    io:format("  ./caberl.escript --channels general,dev --peers localhost:3113~n~n"),
    io:format("  # Use custom storage and port~n"),
    io:format("  ./caberl.escript --storage /tmp/caberl --port 9999 --channels test~n~n"),
    io:format("Notes:~n"),
    io:format("  - Uses the default Cable PSK (cabal key)~n"),
    io:format("  - Keys are generated automatically if they don't exist~n"),
    io:format("  - Press Ctrl+C to stop the peer~n~n").