From 82e0f56922d454f83469b1499a869890de406b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Sat, 15 Jun 2024 02:26:34 +0200 Subject: [PATCH] Refactor --- lib/api/game/advertisement.ml | 32 ++-- lib/client.ml | 124 ++++++++----- lib/data/json.ml | 2 +- lib/logger/async.ml | 12 ++ lib/logger/base.ml | 27 +++ lib/logger/level.ml | 16 ++ lib/logger/sync.ml | 12 ++ .../game/observable_advertisement_member.ml | 22 +-- shell.nix | 1 + .../test_case/game/advertisements.ml | 20 +-- .../res/findObservableAdvertisements.json | 167 ++++++++++++++++++ tests/unit/test_cases/api.ml | 10 ++ 12 files changed, 361 insertions(+), 84 deletions(-) create mode 100644 lib/logger/async.ml create mode 100644 lib/logger/base.ml create mode 100644 lib/logger/level.ml create mode 100644 lib/logger/sync.ml create mode 100644 tests/unit/res/findObservableAdvertisements.json diff --git a/lib/api/game/advertisement.ml b/lib/api/game/advertisement.ml index 7a54053..b8a7e30 100644 --- a/lib/api/game/advertisement.ml +++ b/lib/api/game/advertisement.ml @@ -2,9 +2,10 @@ open Lwt.Syntax open Data.Sort let find_observable ?(start = 1) ?(count = 100) ?(sort = Descending) ?(profile_ids = []) game domain send = + let open Models.Stub.Game.Observable_advertisement_member in let should_descend = match sort with Ascending -> 0 | Descending -> 1 in let base_url = Uri.make ~scheme:"https" ~host:domain ~path:"/game/advertisement/findObservableAdvertisements" () in - let url = + let url profile_ids = Uri.with_query' base_url [ "title", Data.Game.to_str game @@ -23,13 +24,24 @@ let find_observable ?(start = 1) ?(count = 100) ?(sort = Descending) ?(profile_i ; "profileids", Data.Query.encode_lst_i profile_ids ] in - let* json = send url in - match json with - | Some j -> - Lwt.return - @@ Data.Json.try_parse_as - (module Models.Response.Game.Observable_advertisements : Data.Json.JsonParsable - with type t = Models.Response.Game.Observable_advertisements.t) - j - | None -> Lwt.return None + let fetch_and_parse url = + let* json = send url in + match json with + | Some j -> + Lwt.return + @@ Data.Json.try_parse_as + (module Models.Response.Game.Observable_advertisements : Data.Json.JsonParsable + with type t = Models.Response.Game.Observable_advertisements.t) + j + | None -> Lwt.return None + in + let handle_empty_profile_ids () = + let* initial_response = fetch_and_parse (url []) in + match initial_response with + | Some data -> + let ids = List.map (fun member -> member.profile_id) data.members in + fetch_and_parse (url ids) + | None -> Lwt.return None + in + if profile_ids = [] then handle_empty_profile_ids () else fetch_and_parse (url profile_ids) ;; diff --git a/lib/client.ml b/lib/client.ml index 04d3f2a..e1bf1d8 100644 --- a/lib/client.ml +++ b/lib/client.ml @@ -6,65 +6,91 @@ type t = ; cookie : Data.Platform.Cookie.t option } +(** A global request counter is required for the API *) +let request_count = ref 0 + +(** Always use this to access the request count to ensure it increments *) +let count_request () = + request_count := !request_count + 1; + !request_count +;; + +let create_client_with_steam_credentials (domain : string) (game : Data.Game.t) (login : Data.Platform.Steam_login.t) + = + let* cookie = Data.Platform.Cookie.create login domain in + let* _ = + match cookie with + | Some c -> + Logger.Async.info + ~m:"Client" + ~f:"create_client_with_steam_credentials" + "Created new cookie '%s' from steam credentials" + (Data.Platform.Cookie.to_cookie_string c) + | None -> + Logger.Async.error + ~m:"Client" + ~f:"create_client_with_steam_credentials" + "Unable to create cookie from steam credentials" + in + Lwt.return { domain; game; cookie } +;; + let create ?(login = None) ?(cookie = None) domain game = match cookie with - | Some c -> - let* _ = Lwt_io.printl "Creating client using existing cookie" in - Lwt.return { domain; game; cookie = Some c } + | Some _ -> + let* _ = Logger.Async.info ~m:"Client" ~f:"create" "Creating client with existing cookie" in + Lwt.return { domain; game; cookie } | None -> (match login with | Some l -> - let* _ = Lwt_io.printl "Creating client using steam credentials" in - let* cookie = Data.Platform.Cookie.create l domain in - let* _ = - match cookie with - | Some c -> - Lwt_io.printl - @@ Printf.sprintf "Cookie created successfully '%s'" (Data.Platform.Cookie.to_cookie_string c) - | None -> Lwt_io.printl "Unable to create cookie" - in - Lwt.return { domain; game; cookie } - | None -> Lwt.return { domain; game; cookie = None }) + let* _ = Logger.Async.info ~m:"Client" ~f:"create" "Creating client with steam credentials" in + create_client_with_steam_credentials domain game l + | None -> + let* _ = Logger.Async.info ~m:"Client" ~f:"create" "Created unauthenticated client" in + Lwt.return { domain; game; cookie = None }) ;; -let get_json ?(cookie = None) (url : Uri.t) = - let open Data.Platform.Cookie in - let url_with_params = - match cookie with - | None -> url - | Some c -> Uri.add_query_params' url [ "sessionID", c.session_id; "connect_id", c.session_id ] - in - let headers = - match cookie with - | None -> Cohttp.Header.init () - | Some c -> Cohttp.Header.add (Cohttp.Header.init ()) "Cookie" (Data.Platform.Cookie.to_cookie_string c) +let add_cookie_query_to_url (cookie : Data.Platform.Cookie.t option) (url : Uri.t) = + match cookie with + | None -> url + | Some c -> Uri.add_query_params' url [ "sessionID", c.session_id; "connect_id", c.session_id ] +;; + +let add_call_num_to_url (url : Uri.t) = Uri.add_query_params' url [ "callNum", string_of_int @@ count_request () ] + +let create_headers_from_cookie (cookie : Data.Platform.Cookie.t option) = + match cookie with + | None -> Cohttp.Header.init () + | Some c -> Cohttp.Header.add (Cohttp.Header.init ()) "Cookie" (Data.Platform.Cookie.to_cookie_string c) +;; + +let report_http_error (status : Cohttp.Code.status_code) (url : Uri.t) (body : string) = + let* _ = + Logger.Async.error + ~m:"Client" + ~f:"report_http_error" + "HTTP Error: %s for URL: %s. Response: %s" + (Cohttp.Code.string_of_status status) + (Uri.to_string url) + body in - let* resp, body = Cohttp_lwt_unix.Client.get ~headers url_with_params in + Lwt.return_none +;; + +let report_json_body (body_str : string) = + let json = Yojson.Basic.from_string body_str in + Lwt.return_some json +;; + +let get_json ?(cookie = None) (url : Uri.t) = + let url = add_call_num_to_url @@ add_cookie_query_to_url cookie url in + let headers = create_headers_from_cookie cookie in + let* resp, body = Cohttp_lwt_unix.Client.get ~headers url in let status = Cohttp.Response.status resp in + let status_i = Cohttp.Code.code_of_status status in + let* _ = Logger.Async.debug ~m:"Client" ~f:"get_json" "GET json from %s result: %d" (Uri.to_string url) status_i in let* body_str = Cohttp_lwt.Body.to_string body in - let curl_command = - Printf.sprintf - "curl -i -H 'Cookie: %s' '%s'" - (match cookie with Some c -> Data.Platform.Cookie.to_cookie_string c | None -> "") - (Uri.to_string url_with_params) - in - let* _ = Lwt_io.printl curl_command in - if Cohttp.Code.code_of_status status = 200 - then ( - let json = Yojson.Basic.from_string body_str in - Lwt.return (Some json)) - else ( - (* TODO: Find out what to do with this later. What's the return type? Do we have Result? *) - let url_str = Uri.to_string url_with_params in - let* _ = - Lwt_io.printl - @@ Printf.sprintf - "HTTP Error: %s for URL: %s\nResponse: %s" - (Cohttp.Code.string_of_status status) - url_str - body_str - in - Lwt.return None) + if status_i = 200 then report_json_body body_str else report_http_error status url body_str ;; let get ?requester endpoint client = diff --git a/lib/data/json.ml b/lib/data/json.ml index 6a6410a..5e11eff 100644 --- a/lib/data/json.ml +++ b/lib/data/json.ml @@ -7,7 +7,7 @@ end let try_parse_as (type a) (module M : JsonParsable with type t = a) (j : Yojson.Basic.t) : a option = try Some (M.from_json j) with | Yojson.Basic.Util.Type_error (msg, _) -> - Logs.err (fun m -> m "JSON parsing error: %s" msg); + Printf.printf "Json parsing error %s" msg; None | ex -> Logs.err (fun m -> m "Unexpected error: %s" (Printexc.to_string ex)); diff --git a/lib/logger/async.ml b/lib/logger/async.ml new file mode 100644 index 0000000..a9e8509 --- /dev/null +++ b/lib/logger/async.ml @@ -0,0 +1,12 @@ +let log ?m ?f level fmt = + let ksprintf_logger str = + let formatted_msg = Base.format_message ?m ?f level str in + Lwt_io.printf "%s" formatted_msg + in + Printf.ksprintf ksprintf_logger fmt +;; + +let debug ?m ?f fmt = log ?m ?f Level.DEBUG fmt +let info ?m ?f fmt = log ?m ?f Level.INFO fmt +let warn ?m ?f fmt = log ?m ?f Level.WARN fmt +let error ?m ?f fmt = log ?m ?f Level.ERROR fmt diff --git a/lib/logger/base.ml b/lib/logger/base.ml new file mode 100644 index 0000000..745d336 --- /dev/null +++ b/lib/logger/base.ml @@ -0,0 +1,27 @@ +let get_timestamp () = + let open Unix in + let tm = localtime (time ()) in + Printf.sprintf + "%04d-%02d-%02d %02d:%02d:%02d" + (tm.tm_year + 1900) + (tm.tm_mon + 1) + tm.tm_mday + tm.tm_hour + tm.tm_min + tm.tm_sec +;; + +let format_message ?(m = "") ?(f = "") level msg = + let time_str = get_timestamp () in + let level_str = Level.to_string level in + let color = Level.to_color level in + let reset = Level.reset_color in + let location_str = + match m, f with + | "", "" -> "" + | "", f -> Printf.sprintf "[%s]" f + | m, "" -> Printf.sprintf "[%s]" m + | m, f -> Printf.sprintf "[%s::%s]" m f + in + Printf.sprintf "[%s] [%s%s%s] %s %s\n%!" time_str color level_str reset location_str msg +;; diff --git a/lib/logger/level.ml b/lib/logger/level.ml new file mode 100644 index 0000000..81824f8 --- /dev/null +++ b/lib/logger/level.ml @@ -0,0 +1,16 @@ +type t = + | DEBUG + | INFO + | WARN + | ERROR + +let to_string = function DEBUG -> "DBG" | INFO -> "INF" | WARN -> "WAR" | ERROR -> "ERR" + +let to_color = function + | DEBUG -> "\x1b[36m" (* Cyan *) + | INFO -> "\x1b[32m" (* Green *) + | WARN -> "\x1b[33m" (* Yellow *) + | ERROR -> "\x1b[31m" (* Red *) +;; + +let reset_color = "\x1b[0m" diff --git a/lib/logger/sync.ml b/lib/logger/sync.ml new file mode 100644 index 0000000..3984ac9 --- /dev/null +++ b/lib/logger/sync.ml @@ -0,0 +1,12 @@ +let log ?m ?f level fmt = + let kprintf_logger str = + let formatted_msg = Base.format_message ?m ?f level str in + Printf.printf "%s" formatted_msg + in + Printf.ksprintf kprintf_logger fmt +;; + +let debug ?m ?f fmt = log ?m ?f Level.DEBUG fmt +let info ?m ?f fmt = log ?m ?f Level.INFO fmt +let warn ?m ?f fmt = log ?m ?f Level.WARN fmt +let error ?m ?f fmt = log ?m ?f Level.ERROR fmt diff --git a/lib/models/stub/game/observable_advertisement_member.ml b/lib/models/stub/game/observable_advertisement_member.ml index 8a93d74..86cea75 100644 --- a/lib/models/stub/game/observable_advertisement_member.ml +++ b/lib/models/stub/game/observable_advertisement_member.ml @@ -1,36 +1,38 @@ type t = { int1 : int - ; int2 : int + ; profile_id : int ; platform_id : string ; icon : string ; name : string ; string1 : string + ; int2 : int ; int3 : int ; int4 : int ; int5 : int - ; int6 : int ; int_null : int option ; string2 : string - ; int7 : int + ; int6 : int ; list1 : Yojson.Basic.t list } let from_json json = match json with - | `List [ int1; int2; platform_id; icon; name; string1; int3; int4; int5; int6; int_null; string2; int7; list1 ] -> + | `List + [ int1; profile_id; platform_id; icon; name; string1; int2; int3; int4; int5; int_null; string2; int6; list1 ] + -> { int1 = Yojson.Basic.Util.to_int int1 - ; int2 = Yojson.Basic.Util.to_int int2 + ; profile_id = Yojson.Basic.Util.to_int profile_id ; platform_id = Yojson.Basic.Util.to_string platform_id ; icon = Yojson.Basic.Util.to_string icon ; name = Yojson.Basic.Util.to_string name ; string1 = Yojson.Basic.Util.to_string string1 + ; int2 = Yojson.Basic.Util.to_int int2 ; int3 = Yojson.Basic.Util.to_int int3 ; int4 = Yojson.Basic.Util.to_int int4 ; int5 = Yojson.Basic.Util.to_int int5 - ; int6 = Yojson.Basic.Util.to_int int6 ; int_null = Yojson.Basic.Util.to_int_option int_null ; string2 = Yojson.Basic.Util.to_string string2 - ; int7 = Yojson.Basic.Util.to_int int7 + ; int6 = Yojson.Basic.Util.to_int int6 ; list1 = Yojson.Basic.Util.to_list list1 } | _ -> failwith "Unexpected observable advertisement member format" @@ -39,18 +41,18 @@ let from_json json = let to_json n = `List [ `Int n.int1 - ; `Int n.int2 + ; `Int n.profile_id ; `String n.platform_id ; `String n.icon ; `String n.name ; `String n.string1 + ; `Int n.int2 ; `Int n.int3 ; `Int n.int4 ; `Int n.int5 - ; `Int n.int6 ; (match n.int_null with Some i -> `Int i | None -> `Null) ; `String n.string2 - ; `Int n.int7 + ; `Int n.int6 ; `List n.list1 ] ;; diff --git a/shell.nix b/shell.nix index fa269a3..09fea47 100644 --- a/shell.nix +++ b/shell.nix @@ -13,6 +13,7 @@ pkgs.mkShell{ openssl ocamlPackages.ocamlformat ocamlPackages.ocaml-lsp + nodePackages.vscode-json-languageserver pkg-config wireshark zlib diff --git a/tests/integration/test_case/game/advertisements.ml b/tests/integration/test_case/game/advertisements.ml index 50c62d3..bc68013 100644 --- a/tests/integration/test_case/game/advertisements.ml +++ b/tests/integration/test_case/game/advertisements.ml @@ -4,18 +4,10 @@ open Lwt.Syntax let test_get_observable_advertisements test_state _ () = let open Data.Sort in - let endpoint_asc = Api.Game.Advertisement.find_observable ~sort:Ascending ~count:2 in - let endpoint_dsc = Api.Game.Advertisement.find_observable ~sort:Descending ~count:2 in - let* lobbies_asc = Client.get endpoint_asc test_state.client in - let* lobbies_dsc = Client.get endpoint_dsc test_state.client in - match lobbies_asc, lobbies_dsc with - | Some a, Some d -> - let* _ = Lwt_io.printf "Yepge %d\n" (List.length a.advertisements) in - let id_asc = (List.hd a.advertisements).match_id in - let id_dsc = (List.hd d.advertisements).match_id in - Alcotest.(check int) "Different match IDs" id_asc id_dsc; - if id_asc = id_dsc - then Lwt.fail_with "Expected different advertisement IDs in ascending and descending order" - else Lwt.return_unit - | _ -> Lwt.fail_with "No observable advertisements response" + let endpoint = Api.Game.Advertisement.find_observable ~sort:Ascending ~count:1 in + let* lobbies = Client.get endpoint test_state.client in + match lobbies with + | Some l when List.length l.members > 0 && List.length l.advertisements > 0 -> Lwt.return_unit + | Some _ -> Lwt.fail_with "No members or advertisements found in the observable advertisements" + | None -> Lwt.fail_with "No observable advertisements response" ;; diff --git a/tests/unit/res/findObservableAdvertisements.json b/tests/unit/res/findObservableAdvertisements.json new file mode 100644 index 0000000..0144b8c --- /dev/null +++ b/tests/unit/res/findObservableAdvertisements.json @@ -0,0 +1,167 @@ +[ + 0, + [ + [ + 316795854, + 109775242998922540, + 0, + "", + "", + "0", + 1873346, + 1, + "AUTOMATCH", + "AUTOMATCH", + 0, + "megarandom.rms2", + "eNpFUttOwzAM/Zd9QS9Z3njIlIIq5ISOdKi8QTUVMrZMgqldvh4nTrZKlaz4+Bz72KtX0Qn8KjDOixkjKThM6c2eL5sQysNVW5gp3/BPyjPlz+tNiOQwayuaECpz4ECcTJvzhWo6rra/4a0E66qPU3/dv31dd7txVj/b53e5+9uftueP47J5MYUkHjeTdr9oOxZJp9BHFnW0Gfi+S30EbMwfytyHlrd8qaWrUj1XfiR+7xZ66yttW+rd9rmmUhZni/oTB9Om2cNsU8gvIB3+01oRfg0eNQhf6O9iSbGPMWnX2jZJZ+KprtLGlZQf0cPmkXprUFPEGLW5MjQTGODQ9VFfSVeDGCg22IvtE2ZATEfv3jHibhhyt8TdBu4ZZOa87esaZqK+W/R8SJ62uPuWerHAlRVPxAPBlzLhC8RkfJnnAIt+J2/BjhzmqFNj72wzRSwLdUnfR8/Ji1KfDmnPt3vzOE/ec33bmb/vWZlc3y05r43I9fX9XkeW+ZUZ8/x1uFfKT/P9jprsJ8Pbzfqh/mH1D9pA+NY=", + 1, + 2, + "eNq9kFFPgzAUhf0tfUYzqEFd4svcukBER4GOzfhQoISmpRCoJtPsv7s2LsboHnzx6Zx777n3Jp/rOU/voB+6mksWqLq74BWYutdXEF76Dhg11bxTwfzQc4BmtDV24oCalsfBoRpoyYyFrvFK3LNXJj8nSkRUl026623k3NzhLVuxAQ20ZVFic3zEjFY7+8Y8fRltu2WazqmmYAoCgSDO0CzOEIyJ1QUm2OiMpOFjsQw1VrIhbxJvRLV8IP0mET0vSDhuM4RMLs6bO6vZNimQzKNFaPfTdXmzSoJbsHd+wvAm0Pd97xsM7wuGewrGP7OI/8yi+p1FfmTxfPYBiEqqKA==", + 6, + [ + [ + 316795854, + 1873346, + 45118, + 1645659, + 31, + 0, + "/10.0.11.16" + ], + [ + 316795854, + 2036662, + 45067, + 1803602, + 1, + 1, + "/10.0.11.25" + ] + ], + 0, + 512, + 1, + 120, + 1, + 0, + 1717172045, + "ukwest", + null + ], + [ + 316794663, + 109775242997290400, + 0, + "", + "", + "0", + 18187302, + 1, + "AUTOMATCH", + "AUTOMATCH", + 0, + "Arabia.rms", + "eNpFUstOxDAM/Jf9gj6ykThwSJWy6sEJhfRQjlSokC6kEoKWfD1OnHQrVbLs8Yw9zulZ9AK/CozzYsNICg5zytn1pwmhXP60hY3qLX+lOlN+PTchkuOmrWhDqMzCgTiZNusP9fRcPX2HXImcjalfxpfqukB19wD2vQF5/Z0uhVfXtXk0hSQet5H2sGs7FUmn0J8s6mgz8rc+zRGwsb6UeQ4tj3qppfOpnys/Eb93O+WGStuOZrdD7qmUxd2i/szBdGn3sNsc6jtIh/9cK8Kfwbsq4Qv9Uewp9jEm7VrbNunMPPVV2riS6hN62D7QbC1qihijNleGdgIDHPoh6ivpahAjxQZnsUPCjIjpKe8dI+6WIXdH3F3g3kBmzuNef2EnmrtDz8fkaYe372gWC1xZcSEeCL6UCV8gJuPLvAdY9Dt5C3bisEWdGmdnzRyxLPQlfR89Jy9K/bWkOx/vzeM++c71cTN/u7Myub/fc10bkfvr23udWOZXZsr71+G9Un3ebu+ozX4ysId+6L8//QPC0ffA", + 1, + 2, + "eNrNkFFLwzAUhf0teY6ypkznwJe5ZbRYXdM2dhMf0jalIWla2ihM2X93CQ4R9uKbT+fec8+9Fz4PwZdP0A9dLRQPdN1diQrMvZk3u/EnCILRMCM6HSyPJgSGs9aWEwhqVp4Gx25gJXeZqa21fODvXH1PtIyYKZt037vIpb0jWr7hAx5Yy6PE5cRIOKv27o19+jY6u+WGLZlhYA4CiX2S4UWcYT+mTleEEqsLmoZPxTo0RKuGfiiyldX6kfbbRPaioOG4yzC2uThv7p1mu6TAKo9WodtPn8vbTRLcgQM8R2PqXSPk/6KBfmh4/4VG/Gca1Xka+YnG68UXFJiqwA==", + 6, + [ + [ + 316794663, + 18187302, + 44982, + 10484384, + 15, + 0, + "/10.0.11.18" + ], + [ + 316794663, + 18516223, + 44957, + 10632767, + 15, + 1, + "/10.0.11.12" + ] + ], + 0, + 512, + 1, + 120, + 1, + 0, + 1717171682, + "ukwest", + null + ] + ], + [ + [ + 432, + 18516223, + "/steam/76561199609604693", + "{\"icon\":\"PR7-051\"}", + "mr_mystical", + "", + 10632767, + 169, + 1, + 0, + null, + "76561199609604693", + 3, + [] + ], + [ + 9510, + 2036662, + "/steam/76561199038695647", + "{\"icon\":\"PR3-020\"}", + "Dibi", + "xarda", + 1803602, + 514, + 1, + 0, + null, + "76561199038695647", + 3, + [] + ], + [ + 37625, + 1873346, + "/steam/76561198104952493", + "", + "alexanderus", + "420o", + 1645659, + 3699, + 2, + 0, + null, + "76561198104952493", + 3, + [] + ], + [ + 1410, + 18187302, + "/steam/76561199585299110", + "{\"icon\":\"PR5-021\"}", + "BEOWULF", + "", + 10484384, + 301, + 1, + 0, + null, + "76561199585299110", + 3, + [] + ] + ] +] diff --git a/tests/unit/test_cases/api.ml b/tests/unit/test_cases/api.ml index 94a6b97..4941a98 100644 --- a/tests/unit/test_cases/api.ml +++ b/tests/unit/test_cases/api.ml @@ -116,6 +116,16 @@ let test_get_inventory () = | None -> Lwt.fail_with "Expected Some but got None" ;; +let test_get_observable_advertisments () = + let requester = Mock.Json_file.create_requester "findObservableAdvertisements.json" in + let* client = Client.create "aoe-api.worldsedgelink.com" Data.Game.Age4 in + let endpoint = Api.Game.Advertisement.find_observable in + let* response = Client.get endpoint client ~requester in + match response with + | Some r -> Lwt.return @@ Alcotest.(check int) "Response was success" 0 r.status + | None -> Lwt.fail_with "Expected Some but got None" +;; + let test_proxy_request () = let requester = Mock.Json_file.create_requester "proxysteamuserrequest.json" in let* client = Client.create "aoe-api.worldsedgelink.com" Data.Game.Age2 in