Skip to content

Commit

Permalink
Merge pull request #30 from mylesagray/feature/server-queries
Browse files Browse the repository at this point in the history
Initial implementation of server monitoring and querying logic
  • Loading branch information
mylesagray authored Jan 30, 2023
2 parents acbadb8 + ed78ba4 commit 45ce67e
Show file tree
Hide file tree
Showing 10 changed files with 715 additions and 91 deletions.
11 changes: 7 additions & 4 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ name = "pypi"
[packages]
requests = "==2.28.2"
py-cord = "==2.3.2"
steamquery = "==1.0.2"
marshmallow = "*"
marshmallow-dataclass = "*"

[dev-packages]
pylint = "*"
autopep8 = "*"
mypy = "*"
coverage = "*"
pytest-cov = "*"

[requires]
python_version = "3.11"
pytest = "*"
exceptiongroup = "*"

[scripts]
test = "python -m pytest --cov=app --cov-report=html"

[requires]
python_version = "3.10"
test = "python -m pytest --cov=app --cov-report=html"
191 changes: 123 additions & 68 deletions Pipfile.lock

Large diffs are not rendered by default.

32 changes: 19 additions & 13 deletions app/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import requests
import discord
from discord.ext import commands
import gamequery


def env_defined(key):
Expand Down Expand Up @@ -134,12 +135,15 @@ async def _boot(ctx):
@commands.has_any_role(*POWERBOT_ROLE)
async def _shutdown(ctx):
try:
response = requests.get(SHUTDOWN_URL, timeout=2)
if response.status_code == 200:
game = discord.Activity(
name="Powering down...", type=discord.ActivityType.playing)
await bot.change_presence(status=discord.Status.do_not_disturb, activity=game)
await ctx.respond('Server shut down!')
if gamequery.is_anyone_active():
await ctx.respond('Server can\'t be shut down, someone is online!')
else:
response = requests.get(SHUTDOWN_URL, timeout=2)
if response.status_code == 200:
game = discord.Activity(
name="Powering down...", type=discord.ActivityType.playing)
await bot.change_presence(status=discord.Status.do_not_disturb, activity=game)
await ctx.respond('Server shut down!')
except Exception:
await ctx.respond('Server is already offline')
traceback.print_exc()
Expand All @@ -151,13 +155,15 @@ async def _shutdown(ctx):
# https://github.com/Pycord-Development/pycord/issues/974
@commands.has_any_role(*POWERBOT_ROLE)
async def _reboot(ctx):
try:
response = requests.get(REBOOT_URL, timeout=2)
if response.status_code == 200:
game = discord.Activity(
name="Rebooting...", type=discord.ActivityType.playing)
await bot.change_presence(status=discord.Status.streaming, activity=game)
await ctx.respond('Server rebooting!')
if gamequery.is_anyone_active():
await ctx.respond('Server can\'t be rebooted, someone is online!')
else:
response = requests.get(REBOOT_URL, timeout=2)
if response.status_code == 200:
game = discord.Activity(
name="Rebooting...", type=discord.ActivityType.playing)
await bot.change_presence(status=discord.Status.streaming, activity=game)
await ctx.respond('Server rebooting!')
except Exception:
await ctx.respond('Server is already offline')
traceback.print_exc()
Expand Down
136 changes: 136 additions & 0 deletions app/gamequery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""
Queries SteamQuery, DCS and SpaceEngineers instances
"""
import traceback
import logging
from steam import SteamQuery
import network
from servers import Server, ServerType, list_servers, get_server


def is_anyone_active() -> bool:
"""
Checks all known servers for users currently logged in
returns a bool
"""
try:
player_count = 0
for server in list_servers():
server = get_server(server)
player_count += get_players(server).get('current_players')
if player_count > 0:
return True
else:
return False
except:
logging.error("Couldn't query servers for active players")
traceback.print_exc()
raise


def get_players(server: Server) -> dict:
"""
Returns a dict with the current number of players connected
to the server as well as the max players supported
"""
if server['server_type'] is ServerType.STEAM:
try:
steamquery = _steam_server_connection(
server_ip=str(server['ip_address']), port=server['port'])
server_state = _lint_steamquery_output(
steamquery.query_server_info())
return {"current_players": server_state["players"],
"max_players": server_state["max_players"]}

except Exception:
print("Could not get server info")
traceback.print_exc()
raise
elif server['server_type'] is ServerType.SPACE_ENGINEERS:
return {"current_players": 0,
"max_players": 0}

elif server['server_type'] is ServerType.DCS:
return {"current_players": 0,
"max_players": 0}

else:
print(f'Cannot query unrecognised server type {server_type}')


def get_players_details(server: Server) -> list:
"""
Returns a list with all current player objects containing
names, scores and durations on the server
"""
if server['server_type'] is ServerType.STEAM:
try:
steamquery = _steam_server_connection(
server_ip=str(server['ip_address']), port=server['port'])
player_info = _lint_steamquery_output(
steamquery.query_player_info())
return player_info

except Exception:
print("Could not get player info")
traceback.print_exc()
raise
elif server['server_type'] is ServerType.SPACE_ENGINEERS:
pass

elif server['server_type'] is ServerType.DCS:
pass
else:
print(f'Cannot query unrecognised server type {server_type}')


# Creates and returns server connection object


def _steam_server_connection(server_ip: str, port: int) -> object:
"""
Creates a steam query server connection object and passes it back.
"""
try:
# Check if IP address is valid
if not network.valid_ip_address(server_ip):
raise ValueError("IP Address Invalid")

# Check if port is valid
if not network.valid_port(port):
raise ValueError("PORT environment variable is invalid")

# Construct SteamQuery session
print(f'Connecting to {server_ip}:{port}')
server = SteamQuery(server_ip, port)
return server

except Exception:
print("Unable to connect to server")
traceback.print_exc()
raise


def _lint_steamquery_output(query) -> object:
"""
Checks if SteamQuery output should have been an exception
and if so raises one, kill me
"""
# SteamQuery lib returns errors as strings, so need to
# check if "error" key is present to detect exceptions
# when errored, it is always passed back as a dict
#
# If the query is a list, then it is a valid response
# in any case
if isinstance(query, list):
return query
else:
try:
if "error" in query.keys():
raise ConnectionError(str(query))
else:
return query
except Exception:
print("Error passed back from SteamQuery")
traceback.print_exc()
raise
71 changes: 71 additions & 0 deletions app/network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Provides functions for establishing network locations and
communications, port and IP verification
"""
import traceback
from ipaddress import ip_address, IPv6Address, IPv4Address

import requests

# Gets the external IP address where the server is running
# this assumes that the outbound IP after NAT and inbound IP
# before NAT are the same IP address.
#
# Unknown how this will behave in IPv6 environments, but the
# assumption is that it will work just the same as no NAT is
# used in IPv6 and the external IPv6 address will be the
# global address of the machine


def get_external_ip() -> str:
"""
Gets the current external IP of where the app is running.
This uses ifconfig.me and assumes it is not blocked or down.
"""
try:
response = requests.get('https://ifconfig.me/ip', timeout=5)
server_ip = response.content.decode()
print(f'Discovered IP address is {server_ip}')
return str(server_ip)

except Exception:
print("External IP could not be found, ifconfig.me may be down or blocked")
traceback.print_exc()
raise

# Validates if the IP address given is valid


def valid_ip_address(ipaddress: int) -> int:
"""
Checks if the IP address passed is a Valid IPv4 or IPv6 address
"""
try:
if isinstance(ip_address(ipaddress), (IPv4Address, IPv6Address)):
return True
else:
return False

except ValueError:
print("IP address is invalid")
traceback.print_exc()
raise

# Validates if the given port is in valid range


def valid_port(port: int) -> bool:
"""
Checks if a given port is in the valid list of ranges for UDP ports
"""
try:
port = int(port)
if port > 0 and port <= 65535:
print(f'PORT {port} is valid')
return True
raise ValueError(f'PORT {port} is not in valid range 1-65535')

except Exception:
print("PORT is not valid")
traceback.print_exc()
raise
Loading

0 comments on commit 45ce67e

Please sign in to comment.