1
0
Fork 0
mirror of https://codeberg.org/demostf/api.git synced 2026-06-03 18:04:08 +02:00

upload wip

This commit is contained in:
Robin Appelman 2017-01-31 13:34:54 +01:00
commit fca5d7b0a6
15 changed files with 2146 additions and 164 deletions

View file

@ -5,4 +5,14 @@ class BaseController {
$request = \Flight::request();
return isset($request->query[$name]) ? $request->query[$name] : $default;
}
protected function file($name) {
$request = \Flight::request();
return $request->files[$name];
}
protected function post($name, $default = null) {
$request = \Flight::request();
return isset($request->data[$name]) ? $request->data[$name] : $default;
}
}

View file

@ -1,6 +1,5 @@
<?php namespace Controllers;
use flight\net\Route;
use Providers\DemoProvider;
use Providers\MatchProvider;

View file

@ -0,0 +1,85 @@
<?php namespace Controllers;
use Demo\Parser;
use Providers\DemoProvider;
use Providers\UserProvider;
class UploadController extends BaseController {
/**
* @var \Providers\DemoProvider
*/
private $demoProvider;
/**
* @var UserProvider
*/
private $userProvider;
/**
* @var Parser
*/
private $parser;
public function __construct(DemoProvider $demoProvider, UserProvider $userProvider, Parser $parser) {
$this->demoProvider = $demoProvider;
$this->userProvider = $userProvider;
$this->parser = $parser;
}
public function upload() {
$key = $this->post('key');
$red = $this->post('red', 'RED');
$blu = $this->post('blu', 'BLU');
$name = $this->post('name', 'Unnamed');
$demo = $this->file('demo');
$user = $this->userProvider->byKey($key);
if (!$user) {
return 'Invalid key';
}
if (!$red) {
$red = 'RED';
}
if (!$blu) {
$blu = 'BLU';
}
$size = $demo['size'];
if ($size < 1024) {
return 'Demos needs to be at least 1KB is size';
}
if ($size > 100 * 1024 * 1024) {
return 'Demos cant be more than 100MB in size';
}
try {
$info = $this->parser->parseFile($demo['tmp_name']);
} catch (\Exception $e) {
return 'Not a valid demo';
}
if ($info->getDuration() < (5 * 60)) {
return 'Demos need to be at least 5m long';
}
if ($info->getDuration() > (60 * 60)) {
return 'Demos cant be longer than one hour';
}
$tmpPath = $demo->getPathname();
$hash = hash_file('md5', $demo['tmp_name']);
$existingDemo = $this->demoProvider->demoIdByHash($hash);
if ($existingDemo) {
if ($key) {
return 'STV available at: https://demos.tf/' . $existingDemo;
} else {
\Flight::redirect('https://demos.tf/' . $existingDemo);
return '';
}
}
$handle = fopen($tmpPath, 'rb');
$storedDemo = $this->demoProvider->storeDemo($handle, $name);
}
}

42
Demo/AzureStore.php Normal file
View file

@ -0,0 +1,42 @@
<?php namespace Demo;
use MicrosoftAzure\Storage\Blob\Internal\IBlob;
class AzureStore implements IDemoStore {
/**
* @var IBlob
*/
private $blobStorage;
/**
* @param IBlob $blobStorage
*/
public function __construct(IBlob $blobStorage) {
$this->blobStorage = $blobStorage;
}
/**
* @param resource $stream
* @param string $name
* @return StoredDemo
*/
public function store($stream, $name) {
$name = preg_replace("/[^A-Za-z0-9\\.\\-]/", '', $name);
if (substr($name, -4) !== '.dem') {
$name .= '.dem';
}
$id = uniqid() . $name;
$this->upload($stream, $id);
$url = 'https://demostf.blob.core.windows.net/demos/' . $id;
return new StoredDemo('azure', $id, $url);
}
/**
* @param resource $stream
* @param string $id
* @return string mixed
*/
private function upload($stream, $id) {
$this->blobStorage->createBlockBlob('demos', $id, $stream);
}
}

156
Demo/Header.php Normal file
View file

@ -0,0 +1,156 @@
<?php namespace Demo;
/**
* HL2 demo metadata
*/
class Header {
/**
* @var string
*/
protected $type;
/**
* @var int
*/
protected $version;
/**
* @var int
*/
protected $protocol;
/**
* @var string
*/
protected $server;
/**
* @var string
*/
protected $nick;
/**
* @var string
*/
protected $map;
/**
* @var string
*/
protected $game;
/**
* @var float
*/
protected $duration;
/**
* @var int
*/
protected $ticks;
/**
* @var int
*/
protected $frames;
/**
* @var int
*/
protected $sigon;
/**
* @param array $info
*/
public function __construct($info) {
$this->type = $info['type'];
$this->version = $info['version'];
$this->protocol = $info['protocol'];
$this->server = $info['server'];
$this->nick = $info['nick'];
$this->map = $info['map'];
$this->game = $info['game'];
$this->duration = $info['duration'];
$this->ticks = $info['ticks'];
$this->frames = $info['frames'];
$this->sigon = $info['sigon'];
}
/**
* @return float
*/
public function getDuration() {
return $this->duration;
}
/**
* @return int
*/
public function getFrames() {
return $this->frames;
}
/**
* @return string
*/
public function getGame() {
return $this->game;
}
/**
* @return string
*/
public function getMap() {
return $this->map;
}
/**
* @return string
*/
public function getNick() {
return $this->nick;
}
/**
* @return int
*/
public function getProtocol() {
return $this->protocol;
}
/**
* @return string
*/
public function getServer() {
return $this->server;
}
/**
* @return int
*/
public function getSigon() {
return $this->sigon;
}
/**
* @return int
*/
public function getTicks() {
return $this->ticks;
}
/**
* @return string
*/
public function getType() {
return $this->type;
}
/**
* @return int
*/
public function getVersion() {
return $this->version;
}
}

10
Demo/IDemoStore.php Normal file
View file

@ -0,0 +1,10 @@
<?php namespace Demo;
interface IDemoStore {
/**
* @param resource $stream
* @param string $name
* @return StoredDemo
*/
public function store($stream, $name);
}

133
Demo/Parser.php Normal file
View file

@ -0,0 +1,133 @@
<?php namespace Demo;
use GuzzleHttp\Client;
class Parser {
const ANALYSER_BASEURL = 'http://demoserver.azurewebsites.net';
/**
* @param string $head string containing the demo header binary data
* @return Header
* @throws \Exception
*/
public function parseString($head) {
set_error_handler(array($this, 'errorHandler'));
$info = unpack("A8type/Iversion/Iprotocol/A260server/A260nick/A260map/A260game/fduration/Iticks/Iframes/Isigon",
$head);
restore_error_handler();
if ($info['type'] !== 'HL2DEMO') {
throw new \Exception('Not an HL2 demo');
}
return new Header($info);
}
/**
* Parse demo info from a stream
*
* @param resource $stream
* @return Header
* @throws \Exception
*/
public function parseStream($stream) {
$head = fread($stream, 2048);
return $this->parseString($head);
}
/**
* Parse demo info from a local file
*
* @param string $path
* @return Header
* @throws \Exception
*/
public function parseFile($path) {
if (!is_readable($path)) {
throw new \Exception('Unable to open demo: ' . $path);
}
$fh = fopen($path, 'rb');
return $this->parseStream($fh);
}
public function analyse(StoredDemo $storedDemo) {
$endPoint = self::ANALYSER_BASEURL . '/url';
$client = new Client();
$response = $client->post($endPoint, [
'body' => $storedDemo->getUrl()
]);
$data = $response->getBody();
return $this->handleData($data);
}
private function handleData($data) {
if (!is_array($data)) {
throw new \Exception('Error parsing demo');
}
$intervalPerTick = $data['intervalPerTick'];
$red = 0;
$blue = 0;
$chat = [];
$players = [];
foreach ($data['rounds'] as $round) {
if ($round['winner'] === 'red') {
$red++;
} else {
$blue++;
}
}
foreach ($data['chat'] as $message) {
if (isset($message['from'])) {
$chat[] = [
'time' => floor(($message['tick'] - $data['startTick']) * $intervalPerTick),
'from' => $message['from'],
'text' => $message['text']
];
}
}
foreach ($data['users'] as $player) {
$class = 0;
$classSpawns = 0;
foreach ($player['classes'] as $classId => $spawns) {
if ($spawns > $classSpawns) {
$classSpawns = $spawns;
$class = $classId;
}
}
if ($class and $player['steamId']) {//skip spectators
$players[] = [
'name' => $player['name'],
'demo_user_id' => $player['userId'],
'steam_id' => $player['steamId'],
'team' => $player['team'],
'class' => $this->getClassName($class)
];
}
}
return [
'score' => [
'red' => $red,
'blue' => $blue
],
'chat' => $chat,
'players' => $players,
'kills' => $data['deaths']
];
}
private function getClassName($classId) {
$classes = [
1 => 'scout',
2 => 'sniper',
3 => 'soldier',
4 => 'demoman',
5 => 'medic',
6 => 'heavyweapons',
7 => 'pyro',
8 => 'spy',
9 => 'engineer'
];
return isset($classes[$classId]) ? $classes[$classId] : 'Unknown';
}
}

50
Demo/StoredDemo.php Normal file
View file

@ -0,0 +1,50 @@
<?php namespace Demo;
class StoredDemo {
/**
* @var string
*/
private $backend;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $url;
/**
* @param string $backend
* @param string $path
* @param string $url
*/
public function __construct($backend, $path, $url) {
$this->backend = $backend;
$this->path = $path;
$this->url = $url;
}
/**
* @return string
*/
public function getBackend() {
return $this->backend;
}
/**
* @return string
*/
public function getPath() {
return $this->path;
}
/**
* @return string
*/
public function getUrl() {
return $this->url;
}
}

View file

@ -1,5 +1,6 @@
<?php namespace Providers;
use Doctrine\DBAL\Connection;
use RandomLib\Generator;
class AuthProvider extends BaseProvider {
@ -8,7 +9,7 @@ class AuthProvider extends BaseProvider {
*/
private $generator;
public function __construct(\PDO $db, Generator $generator) {
public function __construct(Connection $db, Generator $generator) {
parent::__construct($db);
$this->generator = $generator;
}

View file

@ -1,27 +1,31 @@
<?php namespace Providers;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
use Doctrine\DBAL\Query\QueryBuilder;
use LessQL\Database;
class BaseProvider {
/**
* @var \PDO
* @var Connection
*/
protected $pdo;
protected $connection;
/**
* @var \LessQL\Database
*/
protected $db;
public function __construct(\PDO $pdo) {
$this->pdo = $pdo;
$this->db = new Database($pdo);
public function __construct(Connection $connection) {
$this->connection = $connection;
$this->db = new Database($connection->getWrappedConnection());
$this->dbConfig();
}
private function dbConfig() {
$driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
if ($driver === 'mysql') {
$platform = $this->connection->getDatabasePlatform();
if ($platform instanceof MySqlPlatform) {
$this->db->setIdentifierDelimiter("`");
} else {
$this->db->setIdentifierDelimiter('"');
@ -44,16 +48,23 @@ class BaseProvider {
protected function query($sql, array $params = []) {
$delimiter = $this->db->getIdentifierDelimiter();
$driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
$platform = $this->connection->getDatabasePlatform();
$sql = str_replace('`', $delimiter, $sql);
if ($driver === 'pgsql') {
if ($platform instanceof PostgreSqlPlatform) {
$sql = str_replace('FROM_UNIXTIME(', 'to_timestamp(', $sql);
}
$query = $this->pdo->prepare($sql, $params);
$query = $this->connection->prepare($sql);
$query->execute($params);
return $query;
}
/**
* @return QueryBuilder
*/
protected function getQueryBuilder() {
return new QueryBuilder($this->connection);
}
}

View file

@ -1,6 +1,23 @@
<?php namespace Providers;
use Demo\Header;
use Demo\IDemoStore;
use Demo\StoredDemo;
use Doctrine\DBAL\Connection;
class DemoProvider extends BaseProvider {
const VERSION = 4;
/**
* @var IDemoStore
*/
private $demoStore;
public function __construct(Connection $connection, IDemoStore $demoStore) {
parent::__construct($connection);
$this->demoStore = $demoStore;
}
public function get($id) {
$demo = $this->db->demo()->where('id', $id);
@ -67,23 +84,23 @@ class DemoProvider extends BaseProvider {
}
$offset = ($page - 1) * 50;
$params = [];
$sql = 'SELECT demos.* FROM demos LEFT OUTER JOIN upload_blacklist ON demos.uploader = uploader_id
WHERE upload_blacklist.id IS null';
$query = $this->getQueryBuilder();
$query->select('demos.*')
->from('demos', 'd')
->leftJoin('d', 'upload_blacklist', 'b', $query->expr()->eq('uploader_id', 'uploader'))
->where($query->expr()->isNull('b.id'));
if (isset($where['map'])) {
$sql .= ' AND demos.map = ?';
$params[] = $where['map'];
$query->where($query->expr()->eq('map', $query->createNamedParameter($where['map'])));
}
if (isset($where['playerCount'])) {
$placeholder = implode(', ', array_fill(0, count($where['playerCount']), '?'));
$sql .= ' AND "playerCount" IN (' . $placeholder . ')';
foreach ($where['playerCount'] as $playerCount) {
$params[] = $playerCount;
}
$query->where($query->expr()->in('playerCount', $query->createNamedParameter($where['playerCount'], Connection::PARAM_INT_ARRAY)));
}
$sql .= ' ORDER BY demos.id DESC LIMIT 50 OFFSET ' . $offset;
$result = $this->query($sql, $params);
$demos = $result->fetchAll();
$query->orderBy('demos.tf', 'DESC')
->setMaxResults(50)
->setFirstResult($offset);
$demos = $query->execute()->fetchAll();
return $this->formatList($demos);
}
@ -162,4 +179,43 @@ class DemoProvider extends BaseProvider {
'uploaders' => $result->fetchColumn()
];
}
public function demoIdByHash($hash) {
$query = $this->getQueryBuilder();
$query->select('hash')
->from('demos')
->where($query->expr()->eq('hash', $query->createNamedParameter($hash)));
return $query->execute()->fetchColumn();
}
public function storeDemo($handle, $name) {
$this->demoStore->store($handle, $name);
}
public function save($name, Header $header, StoredDemo $storedDemo, $red, $blu, $uploaderId, $hash) {
$query = $this->getQueryBuilder();
$query->insert('demos')
->values([
'name' => $name,
'url' => $storedDemo->getUrl(),
'map' => $header->getMap(),
'red' => $red,
'blu' => $blu,
'uploader' => $uploaderId,
'duration' => floor($header->getDuration()),
'backend' => $storedDemo->getBackend(),
'path' => $storedDemo->getPath(),
'server' => $header->getServer(),
'nick' => $header->getNick(),
'hash' => $hash,
'version' => 0
]);
$query->execute();
return $this->connection->lastInsertId('demos');
}
// public function analyse()
}

View file

@ -1,5 +1,6 @@
<?php namespace Providers;
use Doctrine\DBAL\Connection;
use RandomLib\Generator;
class UserProvider extends BaseProvider {
@ -8,7 +9,7 @@ class UserProvider extends BaseProvider {
*/
private $generator;
public function __construct(\PDO $db, Generator $generator) {
public function __construct(Connection $db, Generator $generator) {
parent::__construct($db);
$this->generator = $generator;
}
@ -29,17 +30,14 @@ class UserProvider extends BaseProvider {
}
public function get($steamid) {
$user = $this->db->user()->where('steamid', $steamid)->fetch();
if (count($user) < 1) {
return null;
}
return [
'id' => $user['id'],
'steamid' => $user['steamid'],
'name' => $user['name'],
'avatar' => $user['avatar']
];
$query = $this->getQueryBuilder();
$query->select(['id', 'steamid', 'name', 'avatar'])
->from('user')
->where($query->expr()->eq('steamid', $query->createNamedParameter($steamid)));
return $query->execute()->fetch();
}
public function search($query) {
$sql = 'SELECT user_id, players.name, count(demo_id) AS count, steamid,
1-(players.name <-> ?) AS sim FROM players
@ -78,4 +76,13 @@ class UserProvider extends BaseProvider {
return $players;
}
public function byKey($key) {
$query = $this->getQueryBuilder();
$query->select(['id', 'steamid', 'name', 'avatar'])
->from('user')
->where($query->expr()->eq('token', $query->createNamedParameter($key)));
return $query->execute()->fetch();
}
}

21
app.php
View file

@ -2,16 +2,30 @@
$autoloader = require 'vendor/autoload.php';
$autoloader->setPsr4('Providers\\', __DIR__ . '/Providers');
$autoloader->setPsr4('Demo\\', __DIR__ . '/Demo');
$autoloader->setPsr4('Controllers\\', __DIR__ . '/Controllers');
use WindowsAzure\Common\ServicesBuilder;
if (!getenv('DB_TYPE')) {
Dotenv::load(__DIR__);
}
$dsn = getenv('DB_TYPE') . ':dbname=' . getenv('DB_DATABASE') . ';host=' . getenv('DB_HOST');
$db = new \PDO($dsn, getenv('DB_USERNAME'), getenv('DB_PASSWORD'));
$azureConnectionString = 'DefaultEndpointsProtocol=https;AccountName=' . getenv('AZURE_ACCOUNT') . ';AccountKey=' . getenv('AZURE_KEY');
$blobRestProxy = ServicesBuilder::getInstance()->createBlobService($azureConnectionString);
$connectionParams = array(
'dbname' => getenv('DB_DATABASE'),
'user' => getenv('DB_USERNAME'),
'password' => getenv('DB_PASSWORD'),
'host' => getenv('DB_HOST'),
'driver' => getenv('DB_TYPE'),
);
if ($connectionParams['driver'] === 'pgsql') {
$connectionParams['driver'] = 'pdo_pgsql';
}
$db = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);
$demoProvider = new \Providers\DemoProvider($db);
$demoProvider = new \Providers\DemoProvider($db, new \Demo\AzureStore($blobRestProxy));
$factory = new \RandomLib\Factory;
$generator = $factory->getMediumStrengthGenerator();
@ -20,6 +34,7 @@ $userProvider = new \Providers\UserProvider($db, $generator);
$demoController = new \Controllers\DemoController($demoProvider);
$authController = new \Controllers\AuthController($userProvider, $authProvider);
$userController = new \Controllers\UserController($userProvider);
$uploadController = new \Controllers\UploadController($demoProvider, $userProvider, new \Demo\Parser());
Flight::route('/*', function () {
header('Access-Control-Allow-Origin: *');

View file

@ -6,7 +6,10 @@
"symfony/var-dumper": "^2.6",
"ircmaxell/random-lib": "^1.1",
"ehesp/steam-login": "^1.0",
"koraktor/steam-condenser": "^1.3"
"koraktor/steam-condenser": "^1.3",
"microsoft/windowsazure": "^0.5.0",
"guzzlehttp/guzzle": "^6.2",
"doctrine/dbal": "^2.5"
},
"autoload": {
"files": [

1426
composer.lock generated

File diff suppressed because it is too large Load diff