#!/usr/bin/env escript -mode(compile). -include("./ansi.hrl"). % ho from pimp pov -record(pho, {pid = none :: none | pid(), ref = none :: none | reference(), taken = false :: boolean(), name = none :: none | string()}). %% pimp state -record(ps, {lsock = none :: gen_tcp:socket(), hos = [] :: [#pho{}], ho_names = init_ho_names() :: [string()]}). %% ho state -record(hs, {name = none :: string(), daddy = none :: pid(), asock = none :: gen_tcp:socket(), nsock = none :: enoise:noise_socket()}). main([]) -> p_init(6969); main([PortNum]) -> p_init(list_to_integer(PortNum)). noise_options() -> % totally safe crypto Secret = crypto:hash(sha3_256, <<"alice">>), Public = <<116, 159, 91, 248, 138, 250, 73, 40, 231, 50, 81, 110, 137, 163, 44, 76, 48, 130, 225, 95, 168, 121, 93, 44, 148, 42, 180, 103, 11, 40, 168, 96>>, KP = enoise_keypair:new(dh25519, Secret, Public), [{noise, "Noise_NN_25519_ChaChaPoly_BLAKE2b"}, {s, KP}]. % pimp process startup p_init(Port) -> p_logln("pimp startup (p_init)"), Opts = [binary, {packet, 0}, {active, true}], case gen_tcp:listen(Port, Opts) of {ok, LSock} -> ok = p_logfln("pimp starting: ~tp", [self()]), PS_I = #ps{lsock = LSock}, PS_II = p_spawn_ho(PS_I), p_loop(PS_II); {error, closed} -> %% dont be pimp p_logln("listen socket closed") end. p_loop(PS = #ps{hos = Hos}) -> receive {ho, HoPid, taken} -> NewPS = p_taken(HoPid, PS), p_loop(NewPS); {ho, HoPid, sent, Data} -> HoName = p_ho_name(HoPid, PS), SendMessage = io_lib:format("~ts> ~ts\r\n", [HoName, Data]), ok = p_echo_except(HoPid, HoName, SendMessage, Hos), p_loop(PS); Down = {'DOWN', _, _, _, _} -> NewPS = p_ho_down(Down, PS), p_loop(NewPS); Unknown -> ok = p_logfln("unknown message: ~tw", [Unknown]), p_loop(PS) end. p_taken(Pid, PS = #ps{hos = Hos}) -> Hos_II = mark_taken(Pid, Hos, []), PS_II = PS#ps{hos = Hos_II}, PS_III = p_spawn_ho(PS_II), PS_III. mark_taken(Pid, [Ho = #pho{pid = Pid} | Rest], Acc) -> NewHo = Ho#pho{taken = true}, lists:reverse(Acc) ++ [NewHo | Rest]; mark_taken(Pid, [WrongHo | Rest], Acc) -> mark_taken(Pid, Rest, [WrongHo | Acc]); mark_taken(_, [], Acc) -> lists:reverse(Acc). % given pid of ho, get her name p_ho_name(HoPid, #ps{hos = Hos}) -> case lists:keyfind(HoPid, #pho.pid, Hos) of #pho{name = Name} -> Name; false -> "JaneDoe" end. % pimp spawns new ho p_spawn_ho(PS = #ps{lsock = LSock, hos = Hos, ho_names = Names}) -> % new name for ho, initial ho state {N, NewNames} = recycle(Names), InitHS = #hs{name = N, daddy = self()}, % spawn the new ho ok = p_logfln("spawning new ho: ~ts", [N]), SpawnHo = fun() -> h_init(LSock, InitHS) end, {HPid, HRef} = erlang:spawn_monitor(SpawnHo), % new ho record from Pimp POV NewHo = #pho{pid = HPid, ref = HRef, name = N}, ok = p_logfln("spawned new ho: ~tw", [NewHo]), NewPS = PS#ps{hos = [NewHo | Hos], ho_names = NewNames}, NewPS. % ho down, remove her from roster p_ho_down({'DOWN', _Ref, process, Pid, Info}, PS = #ps{hos = Hos}) -> Ho = lists:keyfind(Pid, #pho.pid, Hos), ok = p_logfln("ho down: ~ts; reason: ~tw", [Ho#pho.name, Info]), NewHos = lists:keydelete(Pid, #pho.pid, Hos), NewPS = PS#ps{hos = NewHos}, NewPS. % echo message from one ho to all the rest % skip if equal p_echo_except(SrcPid, SrcName, Data, [#pho{pid = SrcPid} | Rest]) -> p_echo_except(SrcPid, SrcName, Data, Rest); % skip if not taken p_echo_except(SrcPid, SrcName, Data, [#pho{taken = false} | Rest]) -> p_echo_except(SrcPid, SrcName, Data, Rest); p_echo_except(SrcPid, SrcName, Data, [#pho{pid = DstPid, taken = true} | Rest]) -> DstPid ! {pimp, send, Data}, p_echo_except(SrcPid, SrcName, Data, Rest); p_echo_except(_, _, _, []) -> ok. %% runs in ho context h_init(LSock, HS = #hs{name = Name, daddy = Daddy, asock = none, nsock = none}) -> h_logln(Name, "h_init"), % this is key: accept needs to run in ho context, otherwise she % doesn't own the acceptor socket % % blocks until client connects case gen_tcp:accept(LSock) of {ok, ASock} -> h_logln(Name, "taken"), % tell daddy to spawn the next ho Daddy ! {ho, self(), taken}, {ok, NSock, _} = enoise:accept(ASock, noise_options()), Welcome = io_lib:format("HI! I'm ~ts. What's your name?\r\n", [Name]), ok = h_send(Name, NSock, Welcome), HS_II = HS#hs{asock = ASock, nsock = NSock}, h_loop(HS_II); {error, closed} -> h_logln(Name, "didn't get taken") end. h_loop(HS = #hs{name = Name, daddy = Daddy, asock = ASock, nsock = NSock}) -> %ok = inet:setopts(ASock, [{active, once}]), receive {noise, NSock, Data} -> Data_II = string:chomp(Data), ok = h_rcvd(Name, Data_II), Daddy ! {ho, self(), sent, Data_II}, h_loop(HS); {pimp, send, Data} = M -> io:format("~p received ~p~n", [self(), M]), ok = h_send(Name, NSock, Data), h_loop(HS); {tcp_closed, ASock} -> exit(its_fine); Unknown -> ok = h_logfln(Name, "unknown message: ~tw", [Unknown]), h_loop(HS) end. % pimp log in bold p_logln(LogStr) -> Msg = io_lib:format("Daddy: ~ts~n", [LogStr]), PimpChars = ?ANSI_BOLD([Msg]), io:put_chars(PimpChars). p_logfln(LogStr, Args) -> p_logln(io_lib:format(LogStr, Args)). % ho log dimmed h_logln(HoName, LogStr) -> FMsg = io_lib:format("~ts: ~ts~n", [HoName, LogStr]), HoChars = ?ANSI_DIM([FMsg]), io:put_chars(HoChars). h_logfln(HoName, MsgStr, Args) -> h_logln(HoName, io_lib:format(MsgStr, Args)). % normal: john messages h_rcvd(Name, MsgStr) -> Msg = io_lib:format("~ts< ~ts~n", [Name, string:chomp(MsgStr)]), io:put_chars(Msg). h_send(Name, NSock, Msg) -> FMsg = io_lib:format("~ts> ~ts~n", [Name, string:chomp(Msg)]), HoChars = [?ANSI_DIM(FMsg)], ok = io:put_chars(HoChars), Send = unicode:characters_to_binary([string:chomp(Msg), "\r\n"]), ok = enoise:send(NSock, Send). init_ho_names() -> ["Crystal", "Tiffany", "Amber", "Brandy", "Lola", "Angel", "Ginger", "Candy", "Charity", "Anastasia", "Cherry", "Kitty", "Jade", "Destiny", "Devon", "Chastity", "Raven", "Scarlett", "Bambi", "Star", "Paris", "Dallas", "Diamond", "Skye", "Trinity", "Tawny", "Layla", "Lexie", "Roxy", "Porsche", "Nevaeh", "Ashlynn", "Aspen", "Chyna", "Lexus", "Unique", "Chardonnay", "Houston", "London", "Coco", "Luscious", "Delight", "Capri", "Trixie", "Cinnamon"]. recycle([N | Ns]) -> {N, Ns ++ [N]}.