2R3WFEOT3WWS6NFBBABSVRUNUPTXHFFMGPZZQOCPLTD2WB3U55HQC BSISJB2O2HKYGSCX6HIIMLBIXCZ66BRCZH3622G2NOQJRJ5S3HLAC WMCS3B3K2DHIXJUAMFK46GO3CVWBQS3ASBOR6WAVIEOX35GPEQYQC 7CB7WD2VQGU2ZZV3CWXAOFLPM5SY6RADIXRSFLUVXKQXC5472UOAC JMYRP5MUPSBHABHBPIO45F6RC7QSQSNO2EJDUDUIQXX3MEEFDOAQC J32TNYTRQJ3YTGFSI5FXW63R5ACT5SZQ6C4YSHLAZ4HXYAHAN7YQC JVURIEXR72OUGZ2EHP5HB6OMXUNPHMAV66YUUP2444TPOG5XGASQC CBHKQGLDCAH2E4ZNACITBSMADOKPERFCWQPUGMH7UN5TLJXLYI4QC R4JDMB7LL3FLA4NJEAV2DQEXII5XS5KIMG3H4YS5P6W7ZZUE7FIQC QHK3BXHEC6IAAI7HK76Y7W3IQQIEFH7TD7AYMBCLOE7U2L7BT2LQC OJ6KWAG7XUCYNQ6T3FRJK2QOP7DBO26PZEWDVH3Q2MJKIBMNWRPQC 55WLMLEEVBRSTAFRZ5RGF7TOGUF5OPVCPA2TMHAQK45OUO7PA3YQC IHBNW3GI2XB6KAWUYRLL6KDOBUNUOU3N7RRLS6BFNW6SE7ZDHLWQC YWCRGWVDIMCPXBQFM23MKCYZCXJJSD733NJEPB6WUU4G4BSREILAC DZZ4B3UGIYTN3OHAKS2HNCLK7KM2ZSHPZ4JC6YVQL6I2H4KCD5PAC M4TNRFRPHEH6T673JAMJ3CHABASCWMAJVU57HH2XEMDJCB3QPT5QC MONVI5STEDKY5ALVMEXJJXDUX6XQRKTFLP7BBNOQML3VSJEM2JAAC ST7Y5WTYIQNKUOVXW6T5PQZL6R3VUYC37BAEQOA2FL6AYVPR4OLAC RZB6HZ2NI5PIUIWQL63CSKXZGGUG4L6XCSCGR3TI723OO57NDT3AC encode_info_test() ->TestKp = enacl:sign_keypair(),Name = "元気な子",Post = posts:encode(TestKp, [], {info, {name, Name}}),Decoded = posts:decode(Post),[[ {public_key, _Public}, {links, _Links}, {type, 2}, {timestamp, _TimeStamp}, {hash, _Hash}],[ {infos, #{<<"name">> := BinName}}]] = Decoded,GotName = unicode:characters_to_list(BinName),?assertEqual(Name, GotName).
make_test_args() ->S = string:chomp(os:cmd("mktemp --tmpdir -d caberl-peer-XXXXX")),?debugFmt("~nUsing Storage: ~p~n", [S]),[ {listener, ["localhost", 0]}, {storage, S}].
set_nick_and_topic_test() ->[A, B] = [make_test_args() || _ <- lists:seq(1,2)],{ok, Alfi} = peer:start_link(A),{ok, Bert} = peer:start_link(B),{ok, PubAlfi} = peer:node_public_key(Alfi),{ok, PubBert} = peer:node_public_key(Bert),ok = peer:set_nick(Alfi, "Alfi"),ok = peer:set_nick(Bert, "Not Ernie!"),C = "test",peer:join(Alfi, C),peer:join(Bert, C),T = "the mega testing channel",ok = peer:set_topic(Alfi, C, T),%%{ok, PeerAddr} = peer:node_addr(Alfi),%%ok = peer:dial(Bert, PeerAddr),{ok, PeerAddr} = peer:node_addr(Bert),ok = peer:dial(Alfi, PeerAddr),timer:sleep(1000),CheckState = fun(Peer) ->{ok, CState} = peer:channel_state(Peer, C),?assertEqual(T, proplists:get_value(topic, CState)),Members = proplists:get_value(members, CState),?assertEqual(2, maps:size(Members)),{AlfiName, AlfiWhen} = maps:get(PubAlfi, Members),?assertEqual("Alfi", AlfiName),?assert(AlfiWhen > 0),{BertName, BertWhen} = maps:get(PubBert, Members),?assertEqual("Not Ernie!", BertName),?assert(BertWhen > 0)end,lists:foreach(CheckState, [Alfi, Bert]),
%%%%%%% helpers%%%%%make_test_args() ->S = string:chomp(os:cmd("mktemp --tmpdir -d caberl-peer-XXXXX")),?debugFmt("~nUsing Storage: ~p~n", [S]),[ {listener, ["localhost", 0]}, {storage, S}].
encode_body({join, Chan}) ->
encode_body({delete, Hashes}) ->NumHashes = wire:encode_varint(length(Hashes)),lists:foreach(fun(H) -> 32 = length(H) end, Hashes),{1, iolist_to_binary([1, NumHashes, Hashes])};encode_body({info, {name, Name}}) ->NameBin = unicode:characters_to_binary(Name),NameLen = wire:encode_varint(byte_size(NameBin)),{2, iolist_to_binary([4, <<"name">>, NameLen, NameBin, 0])};encode_body({topic, Chan, Topic}) ->
{4, iolist_to_binary([ChanLen, ChanBin])};encode_body({leave, Chan}) ->
TopicData = case length(Topic) of0 -> [0];_ ->TopicBin = unicode:characters_to_binary(Topic),TopicLen = wire:encode_varint(byte_size(TopicBin)),[TopicLen, TopicBin]end,{3, iolist_to_binary([ChanLen, ChanBin] ++ TopicData)};encode_body({JL, Chan}) when JL =:= join; JL =:= leave ->TypeCode = case JL ofjoin -> 4;leave -> 5end,
-export([node_addr/1, dial/2, peer_list/1, channel_list/1, join/2, leave/2, write/3]).
-export([node_addr/1, node_public_key/1, dial/2, peer_list/1]).-export([channels_joined/1, channels_known/1, channel_state/2, join/2, leave/2]).-export([set_topic/3, set_nick/2, write/3]).
channels_known(_Pid) ->{error, todo}.%% gen_server:call(Pid, {channelsQueryKnown}).channel_state(Pid, Chan) ->gen_server:call(Pid, {channelsState, Chan}).set_topic(Pid, Chan, Topic) ->gen_server:call(Pid, {channelsSetTopic, Chan, Topic}).set_nick(Pid, Nick) ->gen_server:call(Pid, {setOwnNick, Nick}).
handle_call({setOwnNick, Nick}, From, State) ->EL = proplists:get_value(eventLoop, State),EL ! {setOwnNick, From, Nick}, % reply via event loop{noreply, State};handle_call({channelsState, Chan}, From, State) ->EL = proplists:get_value(eventLoop, State),EL ! {channelsState, From, Chan}, % reply via event loop{noreply, State};handle_call({channelsSetTopic, Chan, Topic}, From, State) ->EL = proplists:get_value(eventLoop, State),EL ! {channelsSetTopic, From, Chan, Topic}, % reply via event loop{noreply, State};
{setOwnNick, From, Nick} ->%% TODO: get most recent links state for channelLinks = [],Bin = posts:encode(KeyPair, Links, {info, {name, Nick}}),{ok, _, PostHash} = db:save_post(Db, Bin),%% sent out to (all?) open channel state requestsF = fun(ReqId, {Peer, [Header, _]}, AccPeers) ->case proplists:get_value(type, Header) of5 ->{ok, {_, _, Size}} = send_hash_response(Peer, ReqId, [PostHash]),update_peer_sent(AccPeers, Peer, Size);_ -> AccPeersendend,SentPeers = maps:fold(F, Peers, ActiveIn),gen_server:reply(From, ok),event_loop(State#state{peers = SentPeers});
SentPeers = lists:foldl(fun({received, ReqId, Peer}, AccPeers) ->
SentPeers = lists:foldl(fun({Direction, ReqId, Peer}, AccPeers) ->case Direction ofreceived ->{Peer, [Header, _]} = maps:get(ReqId, ActiveIn),case proplists:get_value(type, Header) of4 ->{ok, {_, _, Size}} = send_hash_response(Peer, ReqId, [PostHash]),update_peer_sent(AccPeers, Peer, Size);_ -> AccPeers % ignoredend;sent ->AccPeersendend, Peers, maps:get(Chan, Chans)),gen_server:reply(From, ok),event_loop(State#state{peers = SentPeers})end;{channelsSetTopic, From, Chan, Topic} ->case maps:is_key(Chan, Chans) offalse -> % already joinedgen_server:reply(From, {error, notInChannel}),event_loop(State);true ->%% TODO: get most recent links state for channelLinks = [],Bin = posts:encode(KeyPair, Links, {topic, Chan, Topic}),{ok, _, PostHash} = db:save_post(Db, Bin),SentPeers = lists:foldl(fun({received, ReqId, Peer}, AccPeers) ->
[io:format("[TEMP] got post reply:~n~p~n", [posts:decode(P)]) || P <- Posts],[{ok, _, _} = db:save_post(Db, P) || P <- Posts],
FilterUnknownPosts = fun(Bin) ->%% TODO: avoid double decode by pushing this into the db module[NewHeader, _] = posts:decode(Bin),H = proplists:get_value(hash, NewHeader),HasNot = not db:has_post(Db, H),io:format("[TEMP] got new(~p) post reply:~n~p~n", [HasNot, NewHeader]),HasNotend,NewPosts = lists:filter(FilterUnknownPosts, Posts),[{ok, _, _} = db:save_post(Db, P) || P <- NewPosts],
Qry = "INSERT INTO users (public_key) VALUES (?) ON CONFLICT DO NOTHING;",{rowid, _RowId} = sqlite3:sql_exec(Db, Qry, [{blob, PubKey}]),[{columns, ["id","public_key","name"]},{rows, [{UserId, {blob, PubKey}, _Name}]}] = sqlite3:read(Db, users, {public_key, {blob, PubKey}}),
UserId = get_or_create_user_id(Db, PubKey),
handle_call({channelsHeads, Chan}, _, [{sql, Db}, _] = State) ->Qry = "SELECT hash, user_id from posts where channel = ? and type > 1 order by timestamp desc",QryChan = {blob, list_to_binary(Chan)},[{columns, ["hash", "user_id"]}, {rows, Rows}] = sqlite3:sql_exec(Db, Qry, [QryChan]),JoinLeaveHashes = [Hash || {{blob, Hash}, _} <- Rows],InfoPostQry = "SELECT hash from posts where user_id = ? and type = 2",InfosHashes = lists:foldl(fun({_, UserId}, Acc) ->[{columns, ["hash"]},{rows, InfoRows}] = sqlite3:sql_exec(Db, InfoPostQry, [UserId]),case InfoRows of[{{blob, H}}] ->sets:add_element(H, Acc);[] ->Accendend, sets:new(), Rows),Result = JoinLeaveHashes ++ sets:to_list(InfosHashes),{reply, {ok, Result}, State};
Qry = "SELECT hash from posts where channel = ? and type > 1 order by timestamp asc",QryChan = {blob, list_to_binary(Chan)},[{columns, ["hash"]}, {rows, Rows}] = sqlite3:sql_exec(Db, Qry, [QryChan]),Result = [Hash || {{blob, Hash}} <- Rows],
TopicQry = "SELECT topic from channels where name = ?",%QryChan = {blob, list_to_binary(Chan)},[{columns, ["topic"]}, {rows, [{TopicBin}]}] = sqlite3:sql_exec(Db, TopicQry, [Chan]),Topic = unicode:characters_to_list(TopicBin),MembersQry = "SELECT users.name as name, users.public_key as pub_key, posts.timestamp as timestamp "++ "FROM channel_members "++ "JOIN posts ON channel_members.post_hash = posts.hash "++ "JOIN users ON channel_members.user_id = users.id "++ "WHERE channel_members.channel = ?",io:format("[DEBUG] Members Qry:~n~s~n", [MembersQry]),[ {columns, ["name", "pub_key", "timestamp"]}, {rows, Rows}] = sqlite3:sql_exec(Db, MembersQry, [Chan]),io:format("[DEBUG] Rows: ~p~n", [Rows]),M = lists:foldl(fun(Row, Acc) ->{NameBin, {blob, PubKey}, When} = Row,Name = unicode:characters_to_list(NameBin),Acc#{PubKey => {Name, When}}end, #{}, Rows),Result = [{topic, Topic},{members, M}],
materialize_views(Db, Post) ->[[ {public_key, PubKey}, {links, _Links}, {type, Type}, {timestamp, _TimeStamp}, {hash, Hash}],Body] = Post,UserId = get_or_create_user_id(Db, PubKey),case Type of0 -> ok; %% text: nothing to do1 -> %% delete[ {hashes, Hashes} ] = Body,lists:foreach(fun(H) ->ok = sqlite3:delete(Db, posts, {hash, {blob, H}})end, Hashes),ok;2 -> %% infos[ {infos, InfoMap} ] = Body,%% assert there is nothing else to evaluate{BinName, #{}} = maps:take(<<"name">>, InfoMap),Name = unicode:characters_to_list(BinName),ok = sqlite3:update(Db, users, {id, UserId}, [{name, Name}]),ok;3 -> %% topic[ {channel, Chan}, {topic, TopicBin} ] = Body,Topic = unicode:characters_to_list(TopicBin),ok = sqlite3:update(Db, channels, {name, Chan}, [{topic, Topic}]),ok;4 ->[ {channel, Chan} ] = Body,Res = sqlite3:write(Db, channel_members, [ {channel, Chan}, {user_id, UserId}, {post_hash, {blob, Hash}}]),{rowid, _} = Res,ok;5 ->[ {channel, Chan} ] = Body,Qry = "DELETE FROM channel_members where channel = ? and user_id = ?",ok = sqlite3:sql_exec(Db, Qry, [Chan, UserId]),okend.get_or_create_user_id(Db, PubKey) ->Qry = "INSERT INTO users (public_key, name) VALUES (?, '') ON CONFLICT DO NOTHING;",{rowid, _RowId} = sqlite3:sql_exec(Db, Qry, [{blob, PubKey}]),[{columns, ["id","public_key","name"]},{rows, [{UserId, {blob, PubKey}, _Name}]}] = sqlite3:read(Db, users, {public_key, {blob, PubKey}}),UserId.
users => [ColId, {public_key, blob, [unique, not_null]}, {name, text}]
channel_members => [ColId, {channel, text, not_null}, {user_id, integer, not_null}, {post_hash, blob, not_null}],users => [ColId, {public_key, blob, [unique, not_null]}, {name, text, not_null}]