%%% @private %%% The GajuExpress %%% %%% 0. User opens GajuDesk and selects the key (very top widget) %%% %%% Sending... %%% %%% 1. The user inputs the public key/ID of the party the data should be sent to %%% 2. The user picks the file or directory to send %%% 3. The user decides whether to sign the package or be anonymous %%% 4. The user sets the package's TTL %%% 5. GajuDesk packs up and compresses the bundle. %%% 6. GajuDesk asks GajuExpress for a quote based on size/time %%% 7. The price is shown %%% 8. The user agrees or aborts %%% 9. If the user agrees, a payment window opens and they send a payment to GajuExpress %%% 10. GajuDesk polls until the payment is included or the user gives up %%% 11. Once GajuExpress verifies the payment, the upload begins %%% 12. Progress bar %%% %%% %%% Receiving... %%% %%% 1. User clicks "check for deliveries" %%% 2. GajuExpress issues an ID signature challenge %%% 3. GajuDesk asks GajuExpress whether there are any packages waiting for Key %%% 4. If yes, then the pending deliveries are shown in the delivery panel %%% 5. The user selects + clicks open (or double clicks) a package %%% 6. User selects where to unpack it in a file dialog %%% 7. GajuDesk downloads the package from GajuExpress %%% 8. Progress bar %%% 9. File is decrypted and optionally signature verified %%% 10. GajuExpress deletes their copy of the file %%% @end -module(gd_v_express). -vsn("0.10.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). -behavior(wx_object). %-behavior(gd_v). -include_lib("wx/include/wx.hrl"). -export([to_front/0, to_front/1, trouble/1]). -export([pending/1, accounts/1, retire/2]). -export([start_link/1]). -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). -include("$zx_include/zx_logger.hrl"). -include("gd.hrl"). -include("gdl.hrl"). -record(s, {wx = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(), lang = en :: en | jp, j = none :: none | fun(), prefs = #{} :: map(), keys = #w{} :: #w{}, accs = [] :: [#wr{}], rider = none :: none | pid(), recvr = none :: none | pid(), check = #w{} :: #w{}, list = #w{} :: #w{}, dl = #w{} :: #w{}, dest = none :: none | wx:wx_object(), ttl = none :: none | wx:wx_object(), path = none :: none | wx:wx_object(), sign = none :: none | wx:wx_object(), size = none :: none | wx:wx_object(), cost = none :: none | wx:wx_object(), ul = none :: none | wx:wx_object()}). %-record(mochila, % {tar = <<>> :: binary(), % sig = none :: none | {Key :: binary(), Sig :: binary()}}). %%% Interface -spec to_front() -> ok. to_front() -> wx_object:cast(?MODULE, to_front). -spec to_front(Win) -> ok when Win :: wx:wx_object(). to_front(Win) -> wx_object:cast(Win, to_front). -spec trouble(Info) -> ok when Info :: term(). trouble(Info) -> wx_object:cast(?MODULE, {trouble, Info}). -spec pending(Manifest) -> ok when Manifest :: [Transfer], Transfer :: {Name :: string(), Size :: pos_integer()}. pending(Manifest) -> wx_object:cast(?MODULE, {pending, Manifest}). -spec accounts(Manifest) -> ok when Manifest :: [#wr{}]. accounts(Manifest) -> wx_object:cast(?MODULE, {accounts, Manifest}). -spec retire(PID, Info) -> ok when PID :: pid(), Info :: term(). retire(PID, Info) -> gen_server:cast(?MODULE, {retire, PID, Info}). %%% Startup start_link(Args) -> wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). init({Prefs, {Selected, Keys}}) -> Lang = maps:get(lang, Prefs, en), Trans = gd_jt:read_translations(?MODULE), J = gd_jt:j(Lang, Trans), Wx = wx:new(), Frame = wxFrame:new(Wx, ?wxID_ANY, J("GajuExpress")), Panel = wxWindow:new(Frame, ?wxID_ANY), TopSz = wxBoxSizer:new(?wxVERTICAL), _ = wxBoxSizer:add(TopSz, Panel, zxw:flags(wide)), MainSz = wxBoxSizer:new(?wxVERTICAL), LR_Sz = wxBoxSizer:new(?wxHORIZONTAL), KeyP = wxChoice:new(Panel, ?wxID_ANY, [{choices, Keys}]), KP = #w{name = key_picker, id = wxChoice:getId(KeyP), wx = KeyP}, ZeroBasedSelected = Selected - 1, ok = wxChoice:setSelection(KeyP, ZeroBasedSelected), _ = wxStaticBoxSizer:add(MainSz, KeyP, zxw:flags({base, 5})), DownloadSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Downloads")}]), DownloadBox = wxStaticBoxSizer:getStaticBox(DownloadSz), CheckB = #w{wx = CheckW} = make_button(DownloadBox, check, J("Check for Downloads")), DownloadP = wxListBox:new(DownloadBox, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]), DL_L = #w{name = list, id = wxListBox:getId(DownloadP), wx = DownloadP}, DownloadB = #w{wx = DownloadW} = make_button(DownloadBox, dl, J("Download")), _ = wxButton:disable(DownloadW), _ = wxBoxSizer:add(DownloadSz, CheckW, zxw:flags({base, 5})), _ = wxBoxSizer:add(DownloadSz, DownloadP, zxw:flags({wide, 5})), _ = wxBoxSizer:add(DownloadSz, DownloadW, zxw:flags({base, 5})), UploadSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Uploads")}]), UploadBox = wxStaticBoxSizer:getStaticBox(UploadSz), DestSz = wxStaticBoxSizer:new(?wxHORIZONTAL, UploadBox, [{label, J("Destination ID")}]), DestBox = wxStaticBoxSizer:getStaticBox(DestSz), DestT = wxTextCtrl:new(DestBox, ?wxID_ANY), _ = wxStaticBoxSizer:add(DestSz, DestT, zxw:flags({wide,5})), _ = wxStaticBoxSizer:add(UploadSz, DestSz, zxw:flags({wide,5})), TTL_Sz = wxStaticBoxSizer:new(?wxHORIZONTAL, UploadBox, [{label, J("Transfer Expiration (Days)")}]), TTL_Box = wxStaticBoxSizer:getStaticBox(TTL_Sz), TTL_T = wxTextCtrl:new(TTL_Box, ?wxID_ANY, [{value, "7"}]), _ = wxStaticBoxSizer:add(TTL_Sz, TTL_T, zxw:flags({wide,5})), _ = wxStaticBoxSizer:add(UploadSz, TTL_Sz, zxw:flags({wide,5})), PathSz = wxStaticBoxSizer:new(?wxHORIZONTAL, UploadBox, [{label, J("File or Directory")}]), PathBox = wxStaticBoxSizer:getStaticBox(PathSz), PathP = wxFilePickerCtrl:new(PathBox, ?wxID_ANY), _ = wxStaticBoxSizer:add(PathSz, PathP, zxw:flags({wide,5})), _ = wxStaticBoxSizer:add(UploadSz, PathSz, zxw:flags({wide,5})), SignSz = wxStaticBoxSizer:new(?wxHORIZONTAL, UploadBox, [{label, J("Transfer Signature")}]), SignBox = wxStaticBoxSizer:getStaticBox(SignSz), SignC = wxCheckBox:new(SignBox, ?wxID_ANY, J("Do you want to sign this transfer?")), _ = wxStaticBoxSizer:add(SignSz, SignC, zxw:flags({wide,5})), _ = wxStaticBoxSizer:add(UploadSz, SignSz, zxw:flags({wide,5})), SizeSz = wxStaticBoxSizer:new(?wxHORIZONTAL, UploadBox, [{label, J("Transfer Size")}]), SizeBox = wxStaticBoxSizer:getStaticBox(SizeSz), SizeT = wxStaticText:new(SizeBox, ?wxID_ANY, J("[No File or Directory Selected]")), _ = wxStaticBoxSizer:add(SizeSz, SizeT, zxw:flags({wide,5})), _ = wxStaticBoxSizer:add(UploadSz, SizeSz, zxw:flags({wide,5})), CostSz = wxStaticBoxSizer:new(?wxHORIZONTAL, UploadBox, [{label, J("Cost to Transfer")}]), CostBox = wxStaticBoxSizer:getStaticBox(CostSz), CostT = wxStaticText:new(CostBox, ?wxID_ANY, "[N/A]"), _ = wxStaticBoxSizer:add(CostSz, CostT, zxw:flags({wide,5})), _ = wxStaticBoxSizer:add(UploadSz, CostSz, zxw:flags({wide,5})), UploadB = #w{wx = UploadW} = make_button(UploadBox, ul, J("Upload")), _ = wxButton:disable(UploadW), _ = wxStaticBoxSizer:add(UploadSz, UploadW, zxw:flags({base,5})), _ = wxBoxSizer:add(LR_Sz, DownloadSz, zxw:flags({wide, 5})), _ = wxBoxSizer:add(LR_Sz, UploadSz, zxw:flags({wide, 5})), _ = wxBoxSizer:add(MainSz, LR_Sz, zxw:flags({wide, 5})), ok = wxPanel:setSizer(Panel, MainSz), ok = wxFrame:setSizer(Frame, TopSz), ok = wxSizer:layout(TopSz), NewPrefs = case maps:is_key(geometry, Prefs) of true -> Prefs; false -> Display = wxDisplay:new(), {_, _, W, H} = wxDisplay:getGeometry(Display), ok = wxDisplay:destroy(Display), WW = 500, WH = 350, X = (W div 2) - (WW div 2), Y = (H div 2) - (WH div 2), Prefs#{geometry => {X, Y, WW, WH}} end, ok = gd_v:safe_size(Frame, NewPrefs), ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:connect(Frame, command_choice_selected), ok = wxFrame:connect(Frame, close_window), ok = wxListBox:connect(DownloadP, command_listbox_doubleclicked), true = wxFrame:show(Frame), State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs, keys = KP, accs = Keys, check = CheckB, list = DL_L, dl = DownloadB, dest = DestT, ttl = TTL_T, path = PathP, sign = SignC, size = SizeT, cost = CostT, ul = UploadB}, {Frame, State}. make_button(Parent, Name, Label) -> B = wxButton:new(Parent, ?wxID_ANY, [{label, Label}]), #w{name = Name, id = wxButton:getId(B), wx = B}. %%% wx_object handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. handle_cast({pending, Manifest}, State) -> NewState = do_pending(Manifest, State), {noreply, NewState}; handle_cast({accounts, Manifest}, State) -> NewState = do_accounts(Manifest, State), {noreply, NewState}; handle_cast({retire, PID, Info}, State) -> NewState = do_retire(PID, Info, State), {noreply, NewState}; handle_cast(to_front, State = #s{frame = Frame}) -> ok = ensure_shown(Frame), ok = wxFrame:raise(Frame), {noreply, State}; handle_cast({trouble, Info}, State) -> ok = handle_troubling(State, Info), {noreply, State}; handle_cast(Unexpected, State) -> ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. handle_info(Unexpected, State) -> ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), {noreply, State}. handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{check = #w{id = ID}}) -> NewState = do_check(State), {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{dl = #w{id = ID}}) -> NewState = do_dl(State), {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{ul = #w{id = ID}}) -> NewState = do_ul(State), {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_choice_selected}}, State) -> ok = kill_recvr(State), {noreply, State}; handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked}}, State) -> NewState = do_dl(State), {noreply, NewState}; handle_event(#wx{event = #wxClose{}}, State) -> ok = do_close(State), {noreply, State}; handle_event(Event, State) -> ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), {noreply, State}. handle_troubling(#s{frame = Frame}, Info) -> zxw:show_message(Frame, Info). code_change(_, State, _) -> {ok, State}. terminate(wx_deleted, _) -> wx:destroy(); terminate(Reason, State) -> ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), wx:destroy(). %%% doers do_pending(State, Manifest) -> ok = tell(info, "Would do_pending: ~p", [Manifest]), State. do_accounts(State, Manifest) -> ok = tell(info, "Would do_pending: ~p", [Manifest]), State. do_check(State = #s{recvr = none, keys = #w{wx = KeyP}}) -> case wxChoice:getStringSelection(KeyP) of "" -> State; KeyID -> PubKey = list_to_binary(KeyID), PID = spawn_link(gd_n_recvr, init, [PubKey, {"localhost", 7777}]), do_check2(State#s{recvr = PID}) end; do_check(State = #s{recvr = PID}) -> case gd_n_recvr:check(PID) of ok -> State; {ok, Challenge} -> challenge(State, Challenge) end. do_check2(State = #s{recvr = PID}) -> case gd_n_recvr:check(PID) of {ok, Challenge} -> challenge(State, Challenge); {error, Reason} -> ok = tell(info, "GajuExpress connection failed with: ~p", [Reason]), State#s{recvr = none} end. challenge(State = #s{recvr = PID, keys = #w{wx = KeyP}}, Challenge) -> case wxChoice:getStringSelection(KeyP) of "" -> ok = gd_n_recvr:stop(PID), State; KeyID -> PubKey = list_to_binary(KeyID), prompt_challenge(State, PubKey, Challenge) end. %% TODO: This should really be a live modal instead. %% It needs to have a countdown, possibly a server-side reset negotiation, %% and be able to react to server-side actions like the socket being shut down. prompt_challenge(State = #s{recvr = PID, frame = Frame, j = J}, PubKey, Challenge) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Message Signature Request")), Sizer = wxBoxSizer:new(?wxVERTICAL), Instruction = J("GajuExpress is requesting an authentication signature."), InstTx = wxStaticText:new(Dialog, ?wxID_ANY, Instruction), AcctSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Account")}]), AcctTx = wxStaticText:new(Dialog, ?wxID_ANY, PubKey), _ = wxStaticBoxSizer:add(AcctSz, AcctTx, zxw:flags({wide, 5})), MessSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Message")}]), MessStyle = ?wxTE_MULTILINE bor ?wxTE_READONLY, MessTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{value, Challenge}, {style, MessStyle}]), _ = wxStaticBoxSizer:add(MessSz, MessTx, zxw:flags({wide, 5})), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), Cancel = wxButton:new(Dialog, ?wxID_CANCEL), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags({wide, 5})), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags({wide, 5})), _ = wxBoxSizer:add(Sizer, InstTx, zxw:flags({base, 5})), _ = wxBoxSizer:add(Sizer, AcctSz, zxw:flags({base, 5})), _ = wxBoxSizer:add(Sizer, MessSz, zxw:flags({wide, 5})), _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxDialog:setSize(Dialog, {600, 300}), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:center(Dialog), Outcome = case wxDialog:showModal(Dialog) of ?wxID_OK -> ok; ?wxID_CANCEL -> cancel end, ok = wxDialog:destroy(Dialog), case Outcome of ok -> handle_challenge(State, PubKey, Challenge); cancel -> ok = gd_n_recvr:stop(PID), State end. handle_challenge(State = #s{recvr = PID}, PubKey, Challenge) -> case gd_con:sign_binary(PubKey, Challenge) of {ok, Sig} -> respond(State, Sig); {error, bad_key} -> ok = gd_n_recvr:stop(PID), State end. respond(State = #s{recvr = PID}, Sig) -> ok = case gd_n_recvr:response(PID, Sig) of ok -> ok; {error, Reason} -> ok = tell(info, "~p: ~p", [PID, Reason]) end, State. do_dl(State) -> ok = tell(info, "Would do_dl."), State. do_ul(State) -> ok = tell(info, "Would do_ul."), State. do_close(#s{frame = Frame, prefs = Prefs}) -> Geometry = case wxTopLevelWindow:isMaximized(Frame) of true -> max; false -> {X, Y} = wxWindow:getPosition(Frame), {W, H} = wxWindow:getSize(Frame), {X, Y, W, H} end, NewPrefs = maps:put(geometry, Geometry, Prefs), ok = gd_con:save(?MODULE, NewPrefs), ok = wxWindow:destroy(Frame). %default_name() -> % {{YY, MM, DD}, {Hr, Mn, Sc}} = calendar:local_time(), % Form = "~4.10.0B-~2.10.0B-~2.10.0B_~2.10.0B-~2.10.0B-~2.10.0B", % Name = io_lib:format(Form, [YY, MM, DD, Hr, Mn, Sc]), % unicode:characters_to_list(Name ++ ".gaju"). kill_recvr(#s{recvr = none}) -> ok; kill_recvr(#s{recvr = PID}) -> gd_n_recvr:stop(PID). do_retire(PID, Info, State = #s{rider = PID}) -> ok = tell(info, "Rider retired with: ~p", [Info]), State#s{rider = none}; do_retire(PID, Info, State = #s{recvr = PID}) -> ok = tell(info, "Recvr retired with: ~p", [Info]), State#s{recvr = none}; do_retire(PID, Info, State) -> ok = tell(info, "~p retired with: ~p", [PID, Info]), State. ensure_shown(Frame) -> case wxWindow:isShown(Frame) of true -> ok; false -> true = wxFrame:show(Frame), ok end.