%%%------------------------------------------------------------------- %%% @doc `/browser' API domain -- a direct, session/tab-scoped path %%% to the same typed operations `hk_agent_toolbox' calls for an %%% agent. This is for operator/manual use (opening a session from %%% the frontend's "browser" view without an agent involved) and for %%% tests; it goes through the *same* `hk_browser_session' module %%% and therefore the *same* structured events as agent-driven %%% browser use -- there is exactly one code path that talks to %%% Chromium, regardless of who asked. %%% %%% POST /api/browser/sessions create %%% GET /api/browser/sessions list %%% GET /api/browser/sessions/:id describe %%% POST /api/browser/sessions/:id/close close %%% POST /api/browser/sessions/:id/tabs open a tab %%% POST /api/browser/sessions/:id/tabs/:tab_id/navigate navigate %%% POST /api/browser/sessions/:id/tabs/:tab_id/actions {op, args}: %%% op in back|forward|reload|read_page|query_element|click|type| %%% scroll|screenshot|extract_links|close %%% @end %%%------------------------------------------------------------------- -module(hk_api_browser_h). -export([init/2]). init(Req0 = #{method := <<"POST">>}, collection) -> RequestId = hk_api_util:request_id(Req0), {ok, Body, Req1} = hk_api_util:read_json_body(Req0), OwnerAgentId = maps:get(<<"owner_agent_id">>, Body, undefined), Profile = maps:get(<<"profile">>, Body, <<"manual">>), Req = hk_api_util:run_operation(browser_create_session, RequestId, fun() -> SessionId = hk_id:new(<<"sess">>), case hk_browser_session_sup:start_session(#{session_id => SessionId, owner_agent_id => OwnerAgentId, profile => Profile}) of {ok, _Pid} -> {ok, {browser_session, SessionId}, #{session_id => SessionId}}; {error, Reason} -> {error, Reason} end end, Req1), {ok, Req, collection}; init(Req0 = #{method := <<"GET">>}, collection) -> Sessions = [describe_or_skip(Id) || {Id, _Pid} <- hk_browser_registry:list_sessions()], Req = hk_api_util:reply_json(200, #{sessions => [M || {true, M} <- Sessions]}, Req0), {ok, Req, collection}; init(Req0 = #{method := <<"GET">>}, item) -> SessionId = cowboy_req:binding(id, Req0), Req = case hk_browser_registry:lookup(SessionId) of {ok, Pid} -> {ok, Session} = hk_browser_session:describe(Pid), hk_api_util:reply_json(200, hk_schema:to_map(Session), Req0); {error, not_found} -> hk_api_util:reply_error(404, session_not_found, Req0) end, {ok, Req, item}; init(Req0 = #{method := <<"POST">>}, close) -> SessionId = cowboy_req:binding(id, Req0), RequestId = hk_api_util:request_id(Req0), Req = hk_api_util:run_operation(browser_close_session, RequestId, fun() -> with_session(SessionId, fun(Pid) -> hk_browser_session:close_session(Pid), {ok, {browser_session, SessionId}, #{}} end) end, Req0), {ok, Req, close}; init(Req0 = #{method := <<"POST">>}, tabs) -> SessionId = cowboy_req:binding(id, Req0), RequestId = hk_api_util:request_id(Req0), {ok, Body, Req1} = hk_api_util:read_json_body(Req0), Url = maps:get(<<"url">>, Body, <<"about:blank">>), Req = hk_api_util:run_operation(browser_open_tab, RequestId, fun() -> with_session(SessionId, fun(Pid) -> case hk_browser_session:open_tab(Pid, Url) of {ok, TabId} -> {ok, {browser_tab, TabId}, #{tab_id => TabId}}; {error, Reason} -> {error, Reason} end end) end, Req1), {ok, Req, tabs}; init(Req0 = #{method := <<"POST">>}, navigate) -> SessionId = cowboy_req:binding(id, Req0), TabId = cowboy_req:binding(tab_id, Req0), RequestId = hk_api_util:request_id(Req0), {ok, Body, Req1} = hk_api_util:read_json_body(Req0), Url = maps:get(<<"url">>, Body, <<>>), Req = hk_api_util:run_operation(browser_navigate, RequestId, fun() -> with_session(SessionId, fun(Pid) -> case hk_browser_session:navigate(Pid, TabId, Url) of ok -> {ok, {browser_tab, TabId}, #{}}; {error, Reason} -> {error, Reason} end end) end, Req1), {ok, Req, navigate}; init(Req0 = #{method := <<"POST">>}, actions) -> SessionId = cowboy_req:binding(id, Req0), TabId = cowboy_req:binding(tab_id, Req0), RequestId = hk_api_util:request_id(Req0), {ok, Body, Req1} = hk_api_util:read_json_body(Req0), Op = binary_to_atom(maps:get(<<"op">>, Body, <<"read_page">>), utf8), Args = maps:get(<<"args">>, Body, #{}), Req = hk_api_util:run_operation(browser_tab_action, RequestId, fun() -> with_session(SessionId, fun(Pid) -> dispatch(Pid, Op, TabId, Args) end) end, Req1), {ok, Req, actions}; init(Req0, State) -> Req = hk_api_util:reply_error(405, method_not_allowed, Req0), {ok, Req, State}. %%% internal with_session(undefined, _Fun) -> {error, missing_session_id}; with_session(SessionId, Fun) -> case hk_browser_registry:lookup(SessionId) of {ok, Pid} -> Fun(Pid); {error, not_found} -> {error, session_not_found} end. dispatch(Pid, back, TabId, _Args) -> as_op(hk_browser_session:back(Pid, TabId), TabId); dispatch(Pid, forward, TabId, _Args) -> as_op(hk_browser_session:forward(Pid, TabId), TabId); dispatch(Pid, reload, TabId, _Args) -> as_op(hk_browser_session:reload(Pid, TabId), TabId); dispatch(Pid, close, TabId, _Args) -> as_op(hk_browser_session:close_tab(Pid, TabId), TabId); dispatch(Pid, read_page, TabId, _Args) -> as_result(hk_browser_session:read_page(Pid, TabId), TabId, text); dispatch(Pid, query_element, TabId, Args) -> Selector = maps:get(<<"selector">>, Args, <<>>), as_result(hk_browser_session:query_element(Pid, TabId, Selector), TabId, element); dispatch(Pid, click, TabId, Args) -> Selector = maps:get(<<"selector">>, Args, <<>>), as_result(hk_browser_session:click(Pid, TabId, Selector), TabId, click); dispatch(Pid, type, TabId, Args) -> Selector = maps:get(<<"selector">>, Args, <<>>), Text = maps:get(<<"text">>, Args, <<>>), as_result(hk_browser_session:type(Pid, TabId, Selector, Text), TabId, type); dispatch(Pid, scroll, TabId, Args) -> Dy = maps:get(<<"delta_y">>, Args, 0), as_result(hk_browser_session:scroll(Pid, TabId, Dy), TabId, scroll); dispatch(Pid, screenshot, TabId, _Args) -> as_result(hk_browser_session:screenshot(Pid, TabId), TabId, screenshot_base64); dispatch(Pid, extract_links, TabId, _Args) -> as_result(hk_browser_session:extract_links(Pid, TabId), TabId, links); dispatch(_Pid, Op, _TabId, _Args) -> {error, {unsupported_op, Op}}. as_op(ok, TabId) -> {ok, {browser_tab, TabId}, #{}}; as_op({error, Reason}, _TabId) -> {error, Reason}. as_result({ok, Value}, TabId, Key) -> {ok, {browser_tab, TabId}, #{Key => Value}}; as_result({error, Reason}, _TabId, _Key) -> {error, Reason}. describe_or_skip(SessionId) -> case hk_browser_registry:lookup(SessionId) of {ok, Pid} -> case catch hk_browser_session:describe(Pid) of {ok, Session} -> {true, hk_schema:to_map(Session)}; _ -> false end; _ -> false end.