271 lines
7.8 KiB
Python
271 lines
7.8 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy.orm.exc import NoResultFound
|
|
|
|
from .app import app, db
|
|
|
|
|
|
class Server(db.Model):
|
|
__table_args__ = (db.Index("ix_server_address_port", "address", "port", unique=True),)
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
# World-specific UUID used to identify the server.
|
|
# This is kept secret to prevent anyone from spoofing the server.
|
|
world_uuid = db.Column(db.String(36), nullable=True, index=True, unique=True)
|
|
|
|
# Whether the server is currently online
|
|
online = db.Column(db.Boolean, index=True, nullable=False, default=True)
|
|
|
|
# Server sent connection address
|
|
address = db.Column(db.String, nullable=False)
|
|
port = db.Column(db.Integer, nullable=False, default=30000)
|
|
|
|
# IP address announcement was received from
|
|
announce_ip = db.Column(db.String, nullable=False)
|
|
|
|
# Name of server software. E.g. "minetest"
|
|
server_id = db.Column(db.String, nullable=True)
|
|
|
|
# List of player names, one per line
|
|
clients = db.Column(db.String, nullable=True)
|
|
|
|
# Highest number of clients ever seen
|
|
clients_top = db.Column(db.Integer, nullable=False)
|
|
|
|
# Maximum number of allowed clients
|
|
clients_max = db.Column(db.Integer, nullable=False)
|
|
|
|
# First time that we received an announcement from this server
|
|
first_seen = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
|
|
|
# Time that server sent "start" announcement.
|
|
# This can be used to calculate the current uptime.
|
|
start_time = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
|
|
|
# Time of most recent update request
|
|
last_update = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
|
|
|
# Amount of time that we've seen the server up for, in seconds
|
|
total_uptime = db.Column(db.Float, nullable=False)
|
|
|
|
# Most recent time that the server went down
|
|
down_time = db.Column(db.DateTime, nullable=True)
|
|
|
|
# Server sent value for age of world.
|
|
# Should nearly match uptime on a server that always announces.
|
|
game_time = db.Column(db.Integer, nullable=False)
|
|
|
|
# Server sent value based on sever loop timing
|
|
lag = db.Column(db.Float, nullable=True)
|
|
|
|
# Ping time in seconds
|
|
ping = db.Column(db.Float, nullable=False)
|
|
|
|
# List of enabled mods, one per line
|
|
mods = db.Column(db.String, nullable=True)
|
|
|
|
# Server release version
|
|
version = db.Column(db.String, nullable=False)
|
|
|
|
# Supported protocol versions
|
|
proto_min = db.Column(db.Integer, nullable=False)
|
|
proto_max = db.Column(db.Integer, nullable=False)
|
|
|
|
game_id = db.Column(db.String, nullable=False)
|
|
|
|
# Mapgen name
|
|
mapgen = db.Column(db.String, nullable=True)
|
|
|
|
# Server landing page URL
|
|
url = db.Column(db.String, nullable=True)
|
|
|
|
# Privileges granted to new players by default
|
|
default_privs = db.Column(db.String, nullable=True)
|
|
|
|
name = db.Column(db.String, nullable=False)
|
|
|
|
description = db.Column(db.String, nullable=False)
|
|
|
|
# Roughly the average number of players on the server
|
|
popularity = db.Column(db.Float, nullable=False)
|
|
|
|
# Continent determined from IP
|
|
geo_continent = db.Column(db.String(2), nullable=True)
|
|
|
|
# Flags
|
|
creative = db.Column(db.Boolean, nullable=False)
|
|
is_dedicated = db.Column(db.Boolean, nullable=False)
|
|
damage_enabled = db.Column(db.Boolean, nullable=False)
|
|
pvp_enabled = db.Column(db.Boolean, nullable=False)
|
|
password_required = db.Column(db.Boolean, nullable=False)
|
|
rollback_enabled = db.Column(db.Boolean, nullable=False)
|
|
can_see_far_names = db.Column(db.Boolean, nullable=False)
|
|
|
|
address_verification_required = db.Column(db.Boolean, nullable=False, default=False)
|
|
|
|
@staticmethod
|
|
def find_from_json(obj):
|
|
try:
|
|
if "world_uuid" in obj:
|
|
return Server.query.filter_by(world_uuid=obj["world_uuid"]).one()
|
|
return Server.query.filter_by(address=obj["address"], port=obj["port"]).one()
|
|
except NoResultFound:
|
|
return None
|
|
|
|
@staticmethod
|
|
def create_or_update(obj):
|
|
server = Server.find_from_json(obj)
|
|
if server is not None:
|
|
server.update(obj)
|
|
else:
|
|
server = Server()
|
|
server.update(obj, True)
|
|
db.session.add(server)
|
|
return server
|
|
|
|
def update(self, obj, initial=False):
|
|
now = datetime.now()
|
|
action = obj.get("action", "start")
|
|
|
|
assert action != "delete"
|
|
|
|
if "clients_list" in obj:
|
|
num_clients = len(obj["clients_list"])
|
|
else:
|
|
num_clients = obj["clients"]
|
|
|
|
if initial:
|
|
# Values set only when the server is first created
|
|
assert action == "start"
|
|
self.world_uuid = obj.get("world_uuid")
|
|
self.clients_top = num_clients
|
|
self.total_uptime = 0
|
|
else:
|
|
self.clients_top = max(self.clients_top, num_clients)
|
|
|
|
if action == "start":
|
|
# Fields updated only on startup
|
|
self.start_time = now
|
|
self.mods = "\n".join(obj.get("mods", []))
|
|
self.mapgen = obj.get("mapgen")
|
|
self.default_privs = obj.get("privs")
|
|
self.is_dedicated = obj.get("dedicated", False)
|
|
self.rollback_enabled = obj.get("rollback", False)
|
|
self.can_see_far_names = obj.get("can_see_far_names", False)
|
|
|
|
self.online = True
|
|
|
|
self.address = obj["address"]
|
|
self.port = obj.get("port", 30000)
|
|
|
|
self.announce_ip = obj["ip"]
|
|
|
|
self.server_id = obj.get("server_id")
|
|
|
|
self.clients = "\n".join(obj["clients_list"])
|
|
self.clients_max = obj["clients_max"]
|
|
|
|
self.game_time = obj["game_time"]
|
|
|
|
self.lag = obj.get("lag")
|
|
self.ping = obj["ping"]
|
|
|
|
self.version = obj["version"]
|
|
self.proto_min = obj["proto_min"]
|
|
self.proto_max = obj["proto_max"]
|
|
|
|
self.game_id = obj["gameid"]
|
|
self.url = obj.get("url")
|
|
self.name = obj["name"]
|
|
self.description = obj["description"]
|
|
|
|
if initial:
|
|
self.popularity = num_clients
|
|
else:
|
|
pop_factor = app.config["POPULARITY_FACTOR"]
|
|
self.popularity = self.popularity * pop_factor + \
|
|
num_clients * (1 - pop_factor)
|
|
|
|
self.geo_continent = obj.get("geo_continent")
|
|
|
|
self.creative = obj.get("creative", False)
|
|
self.damage_enabled = obj.get("damage", False)
|
|
self.pvp_enabled = obj.get("pvp", False)
|
|
self.password_required = obj.get("password", False)
|
|
|
|
self.last_update = now
|
|
|
|
if obj["address_verified"]:
|
|
self.address_verification_required = True
|
|
|
|
def as_json(self):
|
|
obj = {
|
|
"address": self.address,
|
|
"can_see_far_names": self.can_see_far_names,
|
|
"clients_list": self.clients.split("\n") if self.clients else [],
|
|
"clients_max": self.clients_max,
|
|
"clients_top": self.clients_top,
|
|
"creative": self.creative,
|
|
"damage": self.damage_enabled,
|
|
"dedicated": self.is_dedicated,
|
|
"description": self.description,
|
|
"game_time": self.game_time,
|
|
"gameid": self.game_id,
|
|
"name": self.name,
|
|
"password": self.password_required,
|
|
"ping": self.ping,
|
|
"pop_v": self.popularity,
|
|
"port": self.port,
|
|
"proto_max": self.proto_max,
|
|
"proto_min": self.proto_min,
|
|
"pvp": self.pvp_enabled,
|
|
"rollback": self.rollback_enabled,
|
|
"uptime": int((datetime.utcnow() - self.start_time).total_seconds()),
|
|
"version": self.version,
|
|
}
|
|
|
|
# Optional fields
|
|
if self.geo_continent is not None:
|
|
obj["geo_continent"] = self.geo_continent
|
|
if self.lag is not None:
|
|
obj["lag"] = self.lag
|
|
if self.mapgen is not None:
|
|
obj["mapgen"] = self.mapgen
|
|
if self.mods is not None:
|
|
obj["mods"] = self.mods.split("\n") if self.mods else []
|
|
if self.default_privs is not None:
|
|
obj["privs"] = self.default_privs
|
|
if self.server_id is not None:
|
|
obj["server_id"] = self.server_id
|
|
if self.url is not None:
|
|
obj["url"] = self.url
|
|
|
|
return obj
|
|
|
|
def set_offline(self):
|
|
now = datetime.utcnow()
|
|
self.online = False
|
|
self.total_uptime += (now - self.start_time).total_seconds()
|
|
self.down_time = now
|
|
|
|
|
|
class Stats(db.Model):
|
|
"""
|
|
This table has only a single row storing all of the global statistics.
|
|
"""
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
max_servers = db.Column(db.Integer, nullable=False, default=0)
|
|
max_clients = db.Column(db.Integer, nullable=False, default=0)
|
|
|
|
@staticmethod
|
|
def get():
|
|
try:
|
|
return Stats.query.filter_by(id=1).one()
|
|
except NoResultFound:
|
|
stats = Stats()
|
|
stats.id = 1
|
|
db.session.add(stats)
|
|
return stats
|