From 4b0fd54a87c0cae2ca9faa93cf3163d6efe6bc99 Mon Sep 17 00:00:00 2001 From: altNightHawk <84723615+altNightHawk@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:39:13 +0000 Subject: [PATCH 1/3] Robustify BattleMetrics fallback: match PHP logic, only return online server for exact IP:port. Clarified error handling and comments. --- discordgsm/protocols/asa.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/discordgsm/protocols/asa.py b/discordgsm/protocols/asa.py index a5d6fae..948bb29 100644 --- a/discordgsm/protocols/asa.py +++ b/discordgsm/protocols/asa.py @@ -38,7 +38,7 @@ async def query(self): host, port = str(self.kv["host"]), int(str(self.kv["port"])) start = time.time() - + # Try EOS first try: eos = opengsq.EOS( @@ -47,7 +47,6 @@ async def query(self): info = await eos.get_info() ping = int((time.time() - start) * 1000) - # Credits: @dkoz https://github.com/DiscordGSM/GameServerMonitor/pull/54/files attributes = dict(info.get("attributes", {})) settings = dict(info.get("settings", {})) @@ -64,37 +63,42 @@ async def query(self): "ping": ping, "raw": info, } - return result except Exception: # EOS failed, fallback to BattleMetrics start = time.time() # Restart timer for BattleMetrics query - + # Fallback: Query BattleMetrics API by IP:port async with aiohttp.ClientSession() as session: url = f"https://api.battlemetrics.com/servers?filter[game]=arksa&filter[search]={host}:{port}" async with session.get(url, timeout=aiohttp.ClientTimeout(total=self.timeout)) as response: if response.status != 200: raise Exception(f"BattleMetrics API returned {response.status}") - + data = await response.json() servers = data.get("data", []) - - # Find the online server matching our IP:port + + # Find the online server matching our IP:port (robust, PHP-parity logic) server_info = None for server in servers: attrs = server.get("attributes", {}) - if attrs.get("ip") == host and attrs.get("port") == port and attrs.get("status") == "online": + # Must match both IP and port, and be online + if ( + attrs.get("ip") == host + and attrs.get("port") == port + and attrs.get("status") == "online" + ): server_info = server break - + if not server_info: + # No online server found for this IP:port raise Exception(f"No online server found on BattleMetrics for {host}:{port}") - + ping = int((time.time() - start) * 1000) attrs = server_info.get("attributes", {}) details = attrs.get("details", {}) - + result: GamedigResult = { "name": attrs.get("name", ""), "map": details.get("map", ""), @@ -108,5 +112,4 @@ async def query(self): "ping": ping, "raw": attrs, } - return result From f23fd3dcd278098d3309a80d065fffaaaa11f37d Mon Sep 17 00:00:00 2001 From: altNightHawk <84723615+altNightHawk@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:40:05 +0000 Subject: [PATCH 2/3] Fix: Add robust BattleMetrics pagination to fallback in ASA protocol. Now checks all pages for online server match. --- discordgsm/protocols/asa.py | 98 +++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/discordgsm/protocols/asa.py b/discordgsm/protocols/asa.py index 948bb29..03bdf59 100644 --- a/discordgsm/protocols/asa.py +++ b/discordgsm/protocols/asa.py @@ -68,48 +68,62 @@ async def query(self): # EOS failed, fallback to BattleMetrics start = time.time() # Restart timer for BattleMetrics query - # Fallback: Query BattleMetrics API by IP:port + # Fallback: Query BattleMetrics API by IP:port, with pagination async with aiohttp.ClientSession() as session: - url = f"https://api.battlemetrics.com/servers?filter[game]=arksa&filter[search]={host}:{port}" - async with session.get(url, timeout=aiohttp.ClientTimeout(total=self.timeout)) as response: - if response.status != 200: - raise Exception(f"BattleMetrics API returned {response.status}") - - data = await response.json() - servers = data.get("data", []) - - # Find the online server matching our IP:port (robust, PHP-parity logic) - server_info = None - for server in servers: - attrs = server.get("attributes", {}) - # Must match both IP and port, and be online - if ( - attrs.get("ip") == host - and attrs.get("port") == port - and attrs.get("status") == "online" - ): - server_info = server + page = 1 + per_page = 100 # BattleMetrics max per page is 100 + server_info = None + while True: + url = ( + f"https://api.battlemetrics.com/servers?filter[game]=arksa&filter[search]={host}:{port}" + f"&page[size]={per_page}&page[number]={page}" + ) + async with session.get(url, timeout=aiohttp.ClientTimeout(total=self.timeout)) as response: + if response.status != 200: + raise Exception(f"BattleMetrics API returned {response.status}") + + data = await response.json() + servers = data.get("data", []) + + # Find the online server matching our IP:port + for server in servers: + attrs = server.get("attributes", {}) + if ( + attrs.get("ip") == host + and attrs.get("port") == port + and attrs.get("status") == "online" + ): + server_info = server + break + if server_info: break - if not server_info: - # No online server found for this IP:port - raise Exception(f"No online server found on BattleMetrics for {host}:{port}") - - ping = int((time.time() - start) * 1000) - attrs = server_info.get("attributes", {}) - details = attrs.get("details", {}) - - result: GamedigResult = { - "name": attrs.get("name", ""), - "map": details.get("map", ""), - "password": details.get("password", False), - "numplayers": attrs.get("players", 0), - "numbots": 0, - "maxplayers": attrs.get("maxPlayers", 0), - "players": None, - "bots": None, - "connect": f"{host}:{port}", - "ping": ping, - "raw": attrs, - } - return result + # Check for next page + meta = data.get("meta", {}) + pagination = meta.get("pagination", {}) + total_pages = pagination.get("totalPages") + if total_pages is None or page >= total_pages: + break + page += 1 + + if not server_info: + raise Exception(f"No online server found on BattleMetrics for {host}:{port}") + + ping = int((time.time() - start) * 1000) + attrs = server_info.get("attributes", {}) + details = attrs.get("details", {}) + + result: GamedigResult = { + "name": attrs.get("name", ""), + "map": details.get("map", ""), + "password": details.get("password", False), + "numplayers": attrs.get("players", 0), + "numbots": 0, + "maxplayers": attrs.get("maxPlayers", 0), + "players": None, + "bots": None, + "connect": f"{host}:{port}", + "ping": ping, + "raw": attrs, + } + return result From 98d4801231bce61f94db854f5f92cd5abd1b2689 Mon Sep 17 00:00:00 2001 From: altNightHawk <84723615+altNightHawk@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:36:56 +0000 Subject: [PATCH 3/3] update battlemeterics api usage --- discordgsm/protocols/asa.py | 156 ++++++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 59 deletions(-) diff --git a/discordgsm/protocols/asa.py b/discordgsm/protocols/asa.py index 03bdf59..1112751 100644 --- a/discordgsm/protocols/asa.py +++ b/discordgsm/protocols/asa.py @@ -68,62 +68,100 @@ async def query(self): # EOS failed, fallback to BattleMetrics start = time.time() # Restart timer for BattleMetrics query - # Fallback: Query BattleMetrics API by IP:port, with pagination - async with aiohttp.ClientSession() as session: - page = 1 - per_page = 100 # BattleMetrics max per page is 100 - server_info = None - while True: - url = ( - f"https://api.battlemetrics.com/servers?filter[game]=arksa&filter[search]={host}:{port}" - f"&page[size]={per_page}&page[number]={page}" - ) - async with session.get(url, timeout=aiohttp.ClientTimeout(total=self.timeout)) as response: - if response.status != 200: - raise Exception(f"BattleMetrics API returned {response.status}") - - data = await response.json() - servers = data.get("data", []) - - # Find the online server matching our IP:port - for server in servers: - attrs = server.get("attributes", {}) - if ( - attrs.get("ip") == host - and attrs.get("port") == port - and attrs.get("status") == "online" - ): - server_info = server - break - if server_info: - break - - # Check for next page - meta = data.get("meta", {}) - pagination = meta.get("pagination", {}) - total_pages = pagination.get("totalPages") - if total_pages is None or page >= total_pages: - break - page += 1 - - if not server_info: - raise Exception(f"No online server found on BattleMetrics for {host}:{port}") - - ping = int((time.time() - start) * 1000) - attrs = server_info.get("attributes", {}) - details = attrs.get("details", {}) - - result: GamedigResult = { - "name": attrs.get("name", ""), - "map": details.get("map", ""), - "password": details.get("password", False), - "numplayers": attrs.get("players", 0), - "numbots": 0, - "maxplayers": attrs.get("maxPlayers", 0), - "players": None, - "bots": None, - "connect": f"{host}:{port}", - "ping": ping, - "raw": attrs, - } - return result + # Fallback: Query BattleMetrics API with multi-stage strategy + # Stage 1: Direct fuzzy search with exact IP:port verification + # Stage 2: Pagination search by game filter only + # Stage 3: Retry with alternative game codes + + async def battlemetrics_lookup(game_codes=None): + """ + Multi-stage BattleMetrics lookup with exact IP:port matching. + Returns server info dict or None if not found. + """ + if game_codes is None: + game_codes = ["arksa", "ark"] + + async with aiohttp.ClientSession() as session: + for game_code in game_codes: + # Stage 1: Direct fuzzy search + try: + url = ( + f"https://api.battlemetrics.com/servers?filter[game]={game_code}" + f"&filter[search]={host}:{port}&page[size]=100" + ) + async with session.get(url, timeout=aiohttp.ClientTimeout(total=self.timeout)) as response: + if response.status == 200: + data = await response.json() + servers = data.get("data", []) + + # Check for exact match in fuzzy results + for server in servers: + attrs = server.get("attributes", {}) + if (attrs.get("ip") == host and + (attrs.get("port") == port or attrs.get("portQuery") == port)): + return server + except Exception: + pass + + # Stage 2: Pagination search by game filter only (more reliable for exact matches) + try: + page = 1 + per_page = 100 + max_pages = 5 + + while page <= max_pages: + url = ( + f"https://api.battlemetrics.com/servers?filter[game]={game_code}" + f"&page[size]={per_page}&page[number]={page}" + ) + async with session.get(url, timeout=aiohttp.ClientTimeout(total=self.timeout)) as response: + if response.status != 200: + break + + data = await response.json() + servers = data.get("data", []) + + if not servers: + break + + # Look for exact match + for server in servers: + attrs = server.get("attributes", {}) + if (attrs.get("ip") == host and + attrs.get("port") == port): + return server + + # Check if there are more pages + meta = data.get("meta", {}) + pagination = meta.get("pagination", {}) + total_pages = pagination.get("totalPages") + if total_pages is None or page >= total_pages: + break + page += 1 + except Exception: + continue + + return None + + server_info = await battlemetrics_lookup() + if not server_info: + raise Exception(f"No server found on BattleMetrics for {host}:{port}") + + ping = int((time.time() - start) * 1000) + attrs = server_info.get("attributes", {}) + details = attrs.get("details", {}) + + result: GamedigResult = { + "name": attrs.get("name", ""), + "map": details.get("map", ""), + "password": details.get("password", False), + "numplayers": attrs.get("players", 0), + "numbots": 0, + "maxplayers": attrs.get("maxPlayers", 0), + "players": None, + "bots": None, + "connect": f"{host}:{port}", + "ping": ping, + "raw": attrs, + } + return result