QTLCENKPK4QOQJTHEAWAJYTJWVH7ZI5KQ2CQTMJRMA4TOMCHTVBAC 6RQQDL46IO2ZFTJSEJREWJIMTNHOH4UBSO2VXAYNLEWNUR72OWHQC BGMVWC2JFYYBR6ESR2JJSBOYEW5HNL5F33ME7MFR74KF3HWWM72AC 755UGKECZ3PFYEA2TFFOUZTF27CRQTBZWO7UYFU6WQDJMZDBVPRAC R4JDMB7LL3FLA4NJEAV2DQEXII5XS5KIMG3H4YS5P6W7ZZUE7FIQC JMYRP5MUPSBHABHBPIO45F6RC7QSQSNO2EJDUDUIQXX3MEEFDOAQC 2R3WFEOT3WWS6NFBBABSVRUNUPTXHFFMGPZZQOCPLTD2WB3U55HQC EDLKGFB5NWTZTHEO6IAR5M4W4533KJI4O6673MKJNTW3JPHQZP5AC J32TNYTRQJ3YTGFSI5FXW63R5ACT5SZQ6C4YSHLAZ4HXYAHAN7YQC IHBNW3GI2XB6KAWUYRLL6KDOBUNUOU3N7RRLS6BFNW6SE7ZDHLWQC 55WLMLEEVBRSTAFRZ5RGF7TOGUF5OPVCPA2TMHAQK45OUO7PA3YQC M4TNRFRPHEH6T673JAMJ3CHABASCWMAJVU57HH2XEMDJCB3QPT5QC NBMKIBO6UJKXCOXXVPPENLEBYI4YOU2VCHH5KIOUGH7WJG47N4PQC DJ7EM5ZXZRSOBHEAA5EVZNVULJCQ7EX4DQBSK2PKPXITWJDPIUXQC JVURIEXR72OUGZ2EHP5HB6OMXUNPHMAV66YUUP2444TPOG5XGASQC DZZ4B3UGIYTN3OHAKS2HNCLK7KM2ZSHPZ4JC6YVQL6I2H4KCD5PAC MONVI5STEDKY5ALVMEXJJXDUX6XQRKTFLP7BBNOQML3VSJEM2JAAC CBHKQGLDCAH2E4ZNACITBSMADOKPERFCWQPUGMH7UN5TLJXLYI4QC EVG6AOW4UUH7C6COH5XHYLPBXC3QEZR2372SE3LJCUPPEZQ3BUQQC YWCRGWVDIMCPXBQFM23MKCYZCXJJSD733NJEPB6WUU4G4BSREILAC T3VWIP2EHZMGXM6UQMK77WB225WRNTAW7IWXZ5ZHUZ6PE3I7AATQC %% Join multiple channelspeer:join(P, "general"),peer:join(P, "random"),peer:join(P, "dev"),%% Should know about all joined channels{ok, Known1} = peer:channels_known(P),?assertEqual(3, length(Known1)),?assert(lists:member("general", Known1)),?assert(lists:member("random", Known1)),?assert(lists:member("dev", Known1)),%% Write a message to one channelok = peer:write(P, "general", "Hello world!"),%% Should still know the same channels{ok, Known2} = peer:channels_known(P),?assertEqual(3, length(Known2)),%% Leave one channelok = peer:leave(P, "dev"),%% Should still know about it (leave post exists){ok, Known3} = peer:channels_known(P),?assertEqual(3, length(Known3)),?assert(lists:member("dev", Known3)),%% But channels_joined should only show 2{ok, Joined} = peer:channels_joined(P),?assertEqual(2, length(Joined)),?assertNot(lists:member("dev", Joined)),ok = peer:stop(P).
%% Alice joins two channels and writes messagespeer:join(Alice, "announcements"),peer:join(Alice, "watercooler"),ok = peer:write(Alice, "announcements", "Important news!"),ok = peer:write(Alice, "watercooler", "How's everyone doing?"),%% Bob joins only one channelpeer:join(Bob, "watercooler"),%% Connect the peers{ok, AliceAddr} = peer:node_addr(Alice),ok = peer:dial(Bob, AliceAddr),timer:sleep(1500),%% Alice should know about both channels she joined{ok, AliceKnown} = peer:channels_known(Alice),?assertEqual(2, length(AliceKnown)),?assert(lists:member("announcements", AliceKnown)),?assert(lists:member("watercooler", AliceKnown)),%% Bob should know about both channels after receiving posts from Alice%% (even though he only joined watercooler){ok, BobKnown} = peer:channels_known(Bob),?debugFmt("~nBob's known channels: ~p~n", [BobKnown]),?assert(length(BobKnown) >= 1),?assert(lists:member("watercooler", BobKnown)),%% Note: Bob might also know about "announcements" depending on sync%% But Bob should only be joined to watercooler{ok, BobJoined} = peer:channels_joined(Bob),?assertEqual(["watercooler"], BobJoined),ok = peer:stop(Alice),ok = peer:stop(Bob).channel_list_request_test() ->[A, B] = [make_test_args(Name) || Name <- ["Alice", "Bob"]],{ok, Alice} = peer:start_link(A),{ok, Bob} = peer:start_link(B),%% Alice joins several channelspeer:join(Alice, "general"),peer:join(Alice, "random"),peer:join(Alice, "dev"),%% Bob joins just onepeer:join(Bob, "announcements"),%% Connect the peers{ok, AliceAddr} = peer:node_addr(Alice),ok = peer:dial(Bob, AliceAddr),timer:sleep(500),%% Bob requests channel list from Aliceok = peer:request_channel_list(Bob, AliceAddr),timer:sleep(500),%% Alice requests channel list from Bob{ok, BobAddr} = peer:node_addr(Bob),ok = peer:request_channel_list(Alice, BobAddr),timer:sleep(500),%% The responses will be logged to console%% (not stored anywhere yet - just testing the protocol works)ok = peer:stop(Alice),ok = peer:stop(Bob).
handle_peer_messages(requestChannelList,{Peer},State = #state{activeOut = ActiveOut, peers = Peers}) ->%% Send a channel list request to the specified peer{ok, {ReqId, _Msg, Size}} = send_channel_list_request(State, Peer, 0, 100),io:format("[Peer] Sent channel list request (~p) to ~p~n", [hex:bin_to_hexstr(ReqId), Peer]),NewActiveOut = maps:put(ReqId, {Peer, channel_list_request}, ActiveOut),NewPeers = update_peer_sent(Peers, Peer, Size),State#state{activeOut = NewActiveOut, peers = NewPeers};
S#state{peers = NewPeers};%% channel list response7 ->Channels = proplists:get_value(channels, Body),io:format("[Peer] Received channel list response from ~p: ~p channels~n",[Peer, length(Channels)]),io:format(" Channels: ~p~n", [Channels]),%% TODO: Could store these in a separate table or emit an event%% For now, just log them
send_binary_to_peer(State, Peer, Binary).send_channel_list_request(State, Peer, Offset, Limit) ->ReqId = crypto:strong_rand_bytes(4),io:format("[Peer] sending channel list request(~p) to ~p (offset:~p, limit:~p)~n",[hex:bin_to_hexstr(ReqId), Peer, Offset, Limit]),Header = [{requestId, ReqId},{circuitId, <<0, 0, 0, 0>>},{ttl, 3}],Binary = wire:encode_channel_list_request(Header, Offset, Limit),
{reply, {ok, Chans}, State};handle_call({channelsKnown}, _From, [{sql, Db}, _] = State) ->%% Get all unique channels from posts table where channel is not nullQuery = "SELECT DISTINCT channel FROM posts WHERE channel IS NOT NULL ORDER BY channel",[{columns, ["channel"]}, {rows, Rows}] = sqlite3:sql_exec(Db, Query),Chans = lists:map(fun({ChannelData}) ->%% Handle both {blob, Bin} and plain BinaryChannelBin = case ChannelData of{blob, B} -> B;B when is_binary(B) -> Bend,binary_to_list(ChannelBin)end,Rows),
ok;terminate(killed, [{sql, Db}, _] = _State) ->io:format("DB Process killed!"),sqlite3:close(Db),
%% 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 argumentscase parse_args(Args) of{ok, Config} ->case maps:get(help, Config, false) oftrue ->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"),
-define(PRINT(Fmt, Args), io:format(Fmt, Args)).
% Start required applicationsok = application:ensure_started(crypto),ok = application:ensure_started(asn1),ok = application:ensure_started(public_key),ok = application:ensure_started(ssl),% Extract configurationStorage = 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 ofundefined -> ok;_ -> io:format(" Nickname: ~s~n", [Nick])end,io:format("~n"),% Start the peerio: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"),
main([Command | CommandArgs]) ->code:add_path("./_build/default/lib/cable/ebin/"),
% Set nickname if providedcase Nick ofundefined ->ok;_ ->io:format("Setting nickname to '~s'...~n", [Nick]),ok = peer:set_nick(PeerPid, Nick)end,
_Args = parse_arguments(CommandArgs),%% invoke the command passed as argumentcase Command of"dial" ->peer:dial("localhost", 3333);"serve" ->peer:start_server("localhost", 3333)
% Wait a bit for channels to be joinedcase length(Channels) of0 -> ok;_ ->timer:sleep(500),io:format("~n")end,% Connect to peerslists:foreach(fun(PeerAddr) ->io:format("Connecting to peer: ~s~n", [PeerAddr]),peer:dial(PeerPid, PeerAddr),timer:sleep(100) % Small delay between connectionsend,Peers),case length(Peers) of0 -> 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 interruptedreceivestop -> okend.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])}
main(Args) ->?PRINT("unknown args: ~p\n", [Args]),erlang:halt(1).
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])}.
parse_arguments([], Acc) ->Acc;parse_arguments(Else, _Acc) ->?PRINT("Args not supported yet ~p \n",[Else]).
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").