#!/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").