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

upload and tests

This commit is contained in:
Robin Appelman 2017-04-10 00:58:50 +02:00
commit 03d3acebf5
21 changed files with 1026 additions and 152 deletions

View file

@ -1,89 +1,22 @@
<?php namespace Demostf\API\Controllers;
use Demostf\API\Demo\DemoStore;
use Demostf\API\Demo\Parser;
use Demostf\API\Providers\DemoProvider;
use Demostf\API\Providers\UserProvider;
use Demostf\API\Providers\UploadProvider;
class UploadController extends BaseController {
private $uploadProvider;
/**
* @var \Providers\DemoProvider
*/
private $demoProvider;
/**
* @var UserProvider
*/
private $userProvider;
/**
* @var Parser
*/
private $parser;
/** @var DemoStore */
private $store;
public function __construct(DemoProvider $demoProvider, UserProvider $userProvider, Parser $parser, DemoStore $store) {
$this->demoProvider = $demoProvider;
$this->userProvider = $userProvider;
$this->parser = $parser;
$this->store = $store;
public function __construct(UploadProvider $uploadProvider) {
$this->uploadProvider = $uploadProvider;
}
public function upload() {
$key = $this->post('key');
$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';
}
$demoFile = $demo['tmp_name'];
$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';
}
$tmpPath = $demo['tmp_name'];
try {
$info = $this->parser->parseHeader($tmpPath);
} 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';
}
$hash = hash_file('md5', $tmpPath);
$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 '';
}
}
$url = $this->store->store($tmpPath, $hash . '_' . $name);
return $this->uploadProvider->upload($key, $red, $blu, $name, $demoFile);
}
}

64
src/Data/ParsedDemo.php Normal file
View file

@ -0,0 +1,64 @@
<?php declare(strict_types=1);
namespace Demostf\API\Data;
use Demostf\API\Demo\ChatMessage;
class ParsedDemo {
/** @var int */
private $redScore;
/** @var int */
private $blueScore;
/** @var ChatMessage[] */
private $chat;
/** @var ParsedPlayer[] */
private $players;
/** @var ParsedKill[] */
private $kills;
/**
* ParsedDemo constructor.
*
* @param int $redScore
* @param int $blueScore
* @param ChatMessage[] $chat
* @param ParsedPlayer[] $players
* @param ParsedKill[] $kills
*/
public function __construct(int $redScore, int $blueScore, array $chat, array $players, array $kills) {
$this->redScore = $redScore;
$this->blueScore = $blueScore;
$this->chat = $chat;
$this->players = $players;
$this->kills = $kills;
}
public function getRedScore(): int {
return $this->redScore;
}
public function getBlueScore(): int {
return $this->blueScore;
}
/**
* @return ChatMessage[]
*/
public function getChat(): array {
return $this->chat;
}
/**
* @return ParsedPlayer[]
*/
public function getPlayers(): array {
return $this->players;
}
/**
* @return ParsedKill[]
*/
public function getKills(): array {
return $this->kills;
}
}

36
src/Data/ParsedKill.php Normal file
View file

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace Demostf\API\Data;
class ParsedKill {
private $attackerDemoId;
private $assisterDemoId;
private $victimDemoId;
private $weapon;
public function __construct(int $attackerDemoId, int $assisterDemoId, int $victimDemoId, string $weapon) {
$this->attackerDemoId = $attackerDemoId;
$this->assisterDemoId = $assisterDemoId;
$this->victimDemoId = $victimDemoId;
$this->weapon = $weapon;
}
public function getAttackerDemoId(): int {
return $this->attackerDemoId;
}
public function getAssisterDemoId(): int {
return $this->assisterDemoId;
}
public function getVictimDemoId(): int {
return $this->victimDemoId;
}
public function getWeapon(): string {
return $this->weapon;
}
}

44
src/Data/ParsedPlayer.php Normal file
View file

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace Demostf\API\Data;
class ParsedPlayer {
/** @var string */
private $name;
/** @var int */
private $demoUserId;
/** @var string */
private $steamId;
/** @var string */
private $team;
/** @var string` */
private $class;
public function __construct(string $name, int $demoUserId, string $steamId, string $team, string $class) {
$this->name = $name;
$this->demoUserId = $demoUserId;
$this->steamId = $steamId;
$this->team = $team;
$this->class = $class;
}
public function getName(): string {
return $this->name;
}
public function getDemoUserId(): int {
return $this->demoUserId;
}
public function getSteamId(): string {
return $this->steamId;
}
public function getTeam(): string {
return $this->team;
}
public function getClass(): string {
return $this->class;
}
}

30
src/Data/StoredDemo.php Normal file
View file

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace Demostf\API\Data;
class StoredDemo {
/** @var string */
private $url;
/** @var string */
private $backend;
/** @var string */
private $path;
public function __construct(string $url, string $backend, string $path) {
$this->url = $url;
$this->backend = $backend;
$this->path = $path;
}
public function getUrl(): string {
return $this->url;
}
public function getBackend(): string {
return $this->backend;
}
public function getPath(): string {
return $this->path;
}
}

44
src/Data/Upload.php Normal file
View file

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace Demostf\API\Data;
class Upload {
/** @var string */
private $name;
/** @var string */
private $red;
/** @var string */
private $blue;
/** @var int */
private $uploaderId;
/** @var string */
private $hash;
public function __construct(string $name, string $red, string $blue, int $uploaderId, string $hash) {
$this->name = $name;
$this->red = $red;
$this->blue = $blue;
$this->uploaderId = $uploaderId;
$this->hash = $hash;
}
public function getName(): string {
return $this->name;
}
public function getRed(): string {
return $this->red;
}
public function getBlue(): string {
return $this->blue;
}
public function getUploaderId(): int {
return $this->uploaderId;
}
public function getHash(): string {
return $this->hash;
}
}

View file

@ -12,7 +12,7 @@ class Demo implements \JsonSerializable {
private $name;
/** @var string */
private $server;
/** @var int */
/** @var float */
private $duration;
/** @var string */
private $nick;
@ -44,7 +44,7 @@ class Demo implements \JsonSerializable {
string $url,
string $name,
string $server,
int $duration,
float $duration,
string $nick,
string $map,
\DateTime $time,
@ -89,7 +89,7 @@ class Demo implements \JsonSerializable {
return $this->server;
}
public function getDuration(): int {
public function getDuration(): float {
return $this->duration;
}
@ -157,6 +157,9 @@ class Demo implements \JsonSerializable {
);
}
/**
* @return DemoPlayer[]
*/
public function getPlayers(): array {
return $this->players;
}

90
src/Demo/DemoSaver.php Normal file
View file

@ -0,0 +1,90 @@
<?php declare(strict_types=1);
namespace Demostf\API\Demo;
use Demostf\API\Data\Kill;
use Demostf\API\Data\ParsedDemo;
use Demostf\API\Data\Player;
use Demostf\API\Data\StoredDemo;
use Demostf\API\Data\Upload;
use Demostf\API\Providers\ChatProvider;
use Demostf\API\Providers\DemoProvider;
use Demostf\API\Providers\KillProvider;
use Demostf\API\Providers\PlayerProvider;
use Demostf\API\Providers\UserProvider;
class DemoSaver {
/** @var KillProvider */
private $killProvider;
/** @var PlayerProvider */
private $playerProvider;
/** @var ChatProvider */
private $chatProvider;
/** @var UserProvider */
private $userProvider;
/** @var DemoProvider */
private $demoProvider;
public function __construct(KillProvider $killProvider, PlayerProvider $playerProvider, ChatProvider $chatProvider, UserProvider $userProvider, DemoProvider $demoProvider) {
$this->killProvider = $killProvider;
$this->playerProvider = $playerProvider;
$this->chatProvider = $chatProvider;
$this->userProvider = $userProvider;
$this->demoProvider = $demoProvider;
}
public function saveDemo(ParsedDemo $demo, Header $header, StoredDemo $storedDemo, Upload $upload): int {
/** @var int[] $userMap [$demoUserId => $dbUserId] */
$userMap = [0 => 0];
$demoId = $this->demoProvider->storeDemo(new Demo(
0,
$storedDemo->getUrl(),
$upload->getName(),
$header->getServer(),
$header->getDuration(),
$header->getNick(),
$header->getMap(),
new \DateTime(),
$upload->getRed(),
$upload->getBlue(),
$demo->getRedScore(),
$demo->getBlueScore(),
count($demo->getPlayers()),
$upload->getUploaderId(),
$upload->getHash()
), $storedDemo->getBackend(), $storedDemo->getPath());
foreach ($demo->getPlayers() as $player) {
$userId = $this->userProvider->getUserId($player->getSteamId());
$userMap[$player->getDemoUserId()] = $userId;
$this->playerProvider->store(new Player(
0,
$demoId,
$player->getDemoUserId(),
$userId,
$player->getName(),
$player->getTeam(),
$player->getClass()
));
}
foreach ($demo->getKills() as $kill) {
$this->killProvider->store(new Kill(
0,
$demoId,
$userMap[$kill->getAttackerDemoId()],
$userMap[$kill->getAssisterDemoId()],
$userMap[$kill->getVictimDemoId()],
$kill->getWeapon()
));
}
foreach ($demo->getChat() as $chat) {
$this->chatProvider->storeChatMessage($demoId, $chat);
}
return $demoId;
}
}

View file

@ -1,5 +1,7 @@
<?php namespace Demostf\API\Demo;
use Demostf\API\Data\StoredDemo;
class DemoStore {
/** @var string */
private $root;
@ -11,13 +13,13 @@ class DemoStore {
$this->webroot = $webroot;
}
public function store(string $sourcePath, string $name): string {
public function store(string $sourcePath, string $name): StoredDemo {
$target = $this->generatePath($name);
if (!is_dir(dirname($target))) {
mkdir(dirname($target), 0777, true);
}
rename($sourcePath, $target);
return $this->getUrl($name);
return new StoredDemo($this->getUrl($name), 'static', $target);
}
private function generatePath(string $name): string {

View file

@ -61,21 +61,30 @@ class Header {
*/
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'];
public function __construct(
string $type,
int $version,
int $protocol,
string $server,
string $nick,
string $map,
string $game,
float $duration,
int $ticks,
int $frames,
int $sigon
) {
$this->type = $type;
$this->version = $version;
$this->protocol = $protocol;
$this->server = $server;
$this->nick = $nick;
$this->map = $map;
$this->game = $game;
$this->duration = $duration;
$this->ticks = $ticks;
$this->frames = $frames;
$this->sigon = $sigon;
}
public function getDuration(): float {
@ -122,4 +131,19 @@ class Header {
return $this->version;
}
public static function fromArray(array $info) {
return new Header(
$info['type'],
$info['version'],
$info['protocol'],
$info['server'],
$info['nick'],
$info['map'],
$info['game'],
$info['duration'],
$info['ticks'],
$info['frames'],
$info['sigon']
);
}
}

View file

@ -14,7 +14,7 @@ class HeaderParser {
if (!isset($info['type']) || $info['type'] !== 'HL2DEMO') {
throw new \InvalidArgumentException('Not an HL2 demo');
}
return new Header($info);
return Header::fromArray($info);
}
/**

View file

@ -2,6 +2,11 @@
namespace Demostf\API\Demo;
use Demostf\API\Data\ParsedDemo;
use Demostf\API\Data\ParsedKill;
use Demostf\API\Data\ParsedPlayer;
use Demostf\API\Data\Player;
/**
* Higher level parser
*
@ -27,7 +32,7 @@ class Parser {
$this->rawParser = $rawParser;
}
public function analyse(string $path): array {
public function analyse(string $path): ParsedDemo {
$data = $this->rawParser->parse($path);
if (!is_array($data)) {
throw new \InvalidArgumentException('Error parsing demo');
@ -35,11 +40,13 @@ class Parser {
return $this->handleData($data);
}
private function handleData(array $data) {
private function handleData(array $data): ParsedDemo {
$intervalPerTick = $data['intervalPerTick'];
$red = 0;
$blue = 0;
/** @var ChatMessage[] $chat */
$chat = [];
/** @var ParsedPlayer[] $players */
$players = [];
foreach ($data['rounds'] as $round) {
if ($round['winner'] === 'red') {
@ -51,11 +58,7 @@ class Parser {
foreach ($data['chat'] as $message) {
if (isset($message['from'])) {
$chat[] = [
'time' => floor(($message['tick'] - $data['startTick']) * $intervalPerTick),
'from' => $message['from'],
'text' => $message['text']
];
$chat[] = new ChatMessage($message['from'], (int)floor(($message['tick'] - $data['startTick']) * $intervalPerTick), $message['text']);
}
}
@ -69,28 +72,59 @@ class Parser {
}
}
if ($class && $player['steamId']) {//skip spectators
$players[] = [
'name' => $player['name'],
'demo_user_id' => $player['userId'],
'steam_id' => $player['steamId'],
'team' => $player['team'],
'class' => $this->getClassName($class)
];
$players[] = new ParsedPlayer(
$player['name'],
$player['userId'],
$this->convertSteamIdToCommunityId($player['steamId']),
$player['team'],
$this->getClassName($class)
);
}
}
return [
'score' => [
'red' => $red,
'blue' => $blue
],
'chat' => $chat,
'players' => $players,
'kills' => $data['deaths']
];
$kills = array_map(function (array $death) {
return new ParsedKill($death['killer'] ?? 0, $death['assister'] ?? 0, $death['victim'] ?? 0, $death['weapon']);
}, $data['deaths']);
return new ParsedDemo(
$red,
$blue,
$chat,
$players,
$kills
);
}
private function getClassName(int $classId): string {
return self::CLASSES[$classId] ?? 'Unknown';
}
/**
* Credit to https://github.com/koraktor/steam-condenser-php
*
* Converts a SteamID as reported by game servers to a 64bit numeric
* SteamID as used by the Steam Community
*
* @param string $steamId The SteamID string as used on servers, like
* <var>STEAM_0:0:12345</var>
* @return string The converted 64bit numeric SteamID
* @throws \InvalidArgumentException if the SteamID doesn't have the correct
* format
*/
public function convertSteamIdToCommunityId($steamId) {
if ($steamId === 'STEAM_ID_LAN' || $steamId === 'BOT') {
throw new \InvalidArgumentException("Cannot convert SteamID \"$steamId\" to a community ID.");
}
if (preg_match('/^STEAM_[0-1]:[0-1]:[0-9]+$/', $steamId)) {
$steamParts = explode(':', substr($steamId, 8));
$steamId = $steamParts[0] + $steamParts[1] * 2 + 1197960265728;
return '7656' . $steamId;
} else if (preg_match('/^\[U:[0-1]:[0-9]+\]$/', $steamId)) {
$steamParts = explode(':', substr($steamId, 3, -1));
$steamId = $steamParts[0] + $steamParts[1] + 1197960265727;
return '7656' . $steamId;
} else {
throw new \InvalidArgumentException("SteamID \"$steamId\" doesn't have the correct format.");
}
}
}

View file

@ -1,4 +1,4 @@
<?php declare(strict_types = 1);
<?php declare(strict_types=1);
namespace Demostf\API\Providers;
@ -65,7 +65,7 @@ class DemoProvider extends BaseProvider {
'red' => $query->createNamedParameter($demo->getRed()),
'blu' => $query->createNamedParameter($demo->getBlue()),
'uploader' => $query->createNamedParameter($demo->getUploader(), \PDO::PARAM_INT),
'duration' => $query->createNamedParameter($demo->getDuration(), \PDO::PARAM_INT),
'duration' => $query->createNamedParameter((int)$demo->getDuration(), \PDO::PARAM_INT),
'created_at' => $query->createNamedParameter($demo->getTime()->format(\DATE_ATOM)),
'updated_at' => 'now()',
'backend' => $query->createNamedParameter($backend),

View file

@ -1,16 +1,109 @@
<?php namespace Demostf\API\Providers;
use Demostf\API\Data\DemoPlayer;
use Demostf\API\Data\ParsedDemo;
use Demostf\API\Data\Upload;
use Demostf\API\Data\User;
use Demostf\API\Demo\DemoSaver;
use Demostf\API\Demo\DemoStore;
use Demostf\API\Demo\Header;
use Demostf\API\Demo\HeaderParser;
use Demostf\API\Demo\Parser;
use Doctrine\DBAL\Connection;
use RandomLib\Generator;
class UploadProvider extends BaseProvider {
/**
* @var Generator
*/
/** @var Generator */
private $generator;
/** @var HeaderParser */
private $headerParser;
/** @var Parser */
private $parser;
/** @var DemoStore */
private $store;
/** @var UserProvider */
private $userProvider;
/** @var DemoProvider */
private $demoProvider;
/** @var DemoSaver */
private $demoSaver;
private $baseUrl;
public function __construct(Connection $db, Generator $generator) {
public function __construct(Connection $db,
string $baseUrl,
HeaderParser $headerParser,
Parser $parser,
DemoStore $store,
UserProvider $userProvider,
DemoProvider $demoProvider,
DemoSaver $demoSaver
) {
parent::__construct($db);
$this->generator = $generator;
$this->baseUrl = $baseUrl;
$this->headerParser = $headerParser;
$this->parser = $parser;
$this->store = $store;
$this->userProvider = $userProvider;
$this->demoProvider = $demoProvider;
$this->demoSaver = $demoSaver;
}
public function upload(string $key, string $red, string $blu, string $name, string $demoFile): string {
$user = $this->userProvider->byKey($key);
if (!$user) {
return 'Invalid key';
}
$hash = hash_file('md5', $demoFile);
$existingDemo = $this->demoProvider->demoIdByHash($hash);
if ($existingDemo) {
return 'STV available at: ' . $this->baseUrl . '/' . $existingDemo;
}
$header = $this->headerParser->parseHeader($demoFile);
$error = $this->validateHeader(filesize($demoFile), $header);
if ($error) {
return $error;
}
$parsed = $this->parser->analyse($demoFile);
$error = $this->validateParsed($header, $parsed);
if ($error) {
return $error;
}
$storedDemo = $this->store->store($demoFile, $hash . '_' . $name);
$upload = new Upload($name, $red, $blu, $user->getId(), $hash);
$id = $this->demoSaver->saveDemo($parsed, $header, $storedDemo, $upload);
return 'STV available at: ' . $this->baseUrl . '/' . $id;
}
public function validateHeader(int $size, Header $header) {
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';
}
if ($header->getDuration() > (60 * 60)) {
return 'Demos cant be longer than one hour';
}
return null;
}
public function validateParsed(Header $header, ParsedDemo $parsedDemo) {
$rounds = $parsedDemo->getRedScore() + $parsedDemo->getBlueScore();
if ($rounds === 0 && $header->getDuration() < (5 * 60)) {
return 'Demos must be at least 5 minutes long';
}
return null;
}
}

View file

@ -92,4 +92,15 @@ class UserProvider extends BaseProvider {
$row = $query->execute()->fetch();
return $row ? User::fromRow($row) : null;
}
public function getUserId(string $steamId) {
$existing = $this->get($steamId);
if ($existing) {
return $existing->getId();
}
$this->store(new \SteamId($steamId));
return $this->get($steamId)->getId();
}
}