WIP: Fortnite Support

parent 25bfebeb
Pipeline #1330 passed with stages
in 2 minutes and 43 seconds
......@@ -87,6 +87,14 @@ class Account extends Model
return $this->hasOne('Onyx\Halo5\Objects\Data', 'account_id', 'id');
}
/**
* @return \Onyx\Fortnite\Objects\Stats
*/
public function fortnite()
{
return $this->hasOne(\Onyx\Fortnite\Objects\Stats::class, 'account_id', 'id');
}
/**
* @return Stats
*/
......
......@@ -2,9 +2,13 @@
namespace Onyx\Fortnite;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Onyx\Account;
use Onyx\Destiny\Helpers\String\Text;
use Onyx\Fortnite\Helpers\Network\FortniteApiNetworkException;
use Onyx\Fortnite\Helpers\Network\Http;
use Onyx\Fortnite\Objects\Stats;
use Onyx\XboxLive\Enums\Console;
/**
* Class Client.
......@@ -14,10 +18,12 @@ class Client extends Http
/**
* @param Account $account
* @param string $id
* @return array
* @return Stats
* @throws FortniteApiNetworkException
* @throws \Exception
* @throws \Throwable
*/
public function getAccountRoyaleStats(Account $account, string $id)
public function getAccountRoyaleStats(Account $account, string $id): Stats
{
$url = sprintf(Constants::$PvP, $id);
......@@ -25,31 +31,125 @@ class Client extends Http
$platform = $this->getPlatformType($data);
$normalized = $this->statNormalizer($data, $platform);
$stats = $this->getStatsModel($id, $account);
if ($this->updateStatsModel($stats, $normalized, $platform)) {
return $stats;
}
throw new FortniteApiNetworkException();
}
/**
* @param string $id
* @return string
* @throws \Exception
*/
public function getPlatformViaEndpoint(string $id): string
{
$url = sprintf(Constants::$PvP, $id);
return $normalized;
$data = $this->getJson($url);
return $this->getPlatformType($data);
}
/**
* @param string $name
* @param string $platform
* @return array
* @throws FortniteApiNetworkException
* @throws \Exception
* @throws \Throwable
*/
public function getAccountByTag(string $name, string $platform): array
{
$url = sprintf(Constants::$lookup, $name);
$expectedPlatform = Console::getFortniteTag($platform);
$data = $this->getJson($url);
return [$data['id'], new Account()];
if (isset($data['id'])) {
$this->checkPlatforms($expectedPlatform, $this->getPlatformViaEndpoint($data['id']));
// Load a specific account based on platform
try {
$account = Account::where('seo', Text::seoGamertag($data['displayName']))
->where('accountType', $platform)
->firstOrFail();
} catch (ModelNotFoundException $ex) {
$account = new Account([
'gamertag' => $data['displayName'],
'accountType' => $platform
]);
$account->saveOrFail();
}
}
return [$data['id'], $account];
}
//---------------------------------------------------------------------------------
// Private Functions
//---------------------------------------------------------------------------------
private function getStatsModel(string $id): Stats
/**
* @param string $expected
* @param string $obtained
* @throws FortniteApiNetworkException
*/
private function checkPlatforms(string $expected, string $obtained): void
{
if ($expected !== $obtained) {
throw new FortniteApiNetworkException();
}
}
/**
* @param string $id
* @param Account|null $account
* @return Stats
*/
private function getStatsModel(string $id, Account $account = null): Stats
{
try {
$stats = Stats::where('epic_id', $id)->firstOrFail();
} catch (ModelNotFoundException $ex) {
$stats = new Stats([
'epic_id' => $id
]);
if ($account !== null) {
$stats->account_id = $account->id;
}
}
return $stats;
}
/**
* @param Stats $statModel
* @param array $normalized
* @param string $platform
* @return bool
* @throws \Throwable
*/
private function updateStatsModel(Stats $statModel, array $normalized, string $platform): bool
{
// TODO create Stat model
$allowedAttributes = ['kills', 'matchesplayed', 'score', 'minutesplayed', 'lastmodified', 'top1', 'top3', 'top5',
'top6', 'top10', 'top12', 'top25'];
foreach ($normalized[$platform] as $group => $stats) {
foreach ($stats as $key => $item) {
if (! in_array($key, $allowedAttributes)) {
continue;
}
$key = $group . '_' . $key;
$statModel->setAttribute($key, $item['alltime']);
}
}
return $statModel->saveOrFail();
}
/**
......
......@@ -35,14 +35,20 @@ class Http
$this->guzzle = new Guzzle();
}
public function getJson($url): ?array
public function getJson($url, $minutes = 5): ?array
{
$key = md5($url);
$this->determineoAuthStatus();
if (!$this->guzzle instanceof Guzzle) {
$this->setupGuzzle();
}
if (\Cache::has($key)) {
return \Cache::get($key);
}
try {
$response = $this->guzzle->get($url, [
'headers' => [
......@@ -55,7 +61,12 @@ class Http
throw new FortniteApiNetworkException();
}
return json_decode($response->getBody(), true);
$data = \GuzzleHttp\json_decode($response->getBody(), true);
if ($minutes > 0) {
\Cache::put($key, $data, $minutes);
}
return $data;
} catch (\Exception $ex) {
return null;
}
......
......@@ -2,11 +2,58 @@
namespace Onyx\Fortnite\Objects;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Onyx\Account;
use Onyx\User;
/**
* Class Stats
* @package Onyx\Fortnite\Objects
* @property int $id
* @property string $epic_id
* @property int $account_id
* @property int $user_id
* @property int $solo_kills
* @property int $solo_matchesplayed
* @property int $solo_score
* @property int $solo_minutesplayed
* @property Carbon $solo_lastmodified
* @property int $solo_top1
* @property int $solo_top3
* @property int $solo_top5
* @property int $solo_top6
* @property int $solo_top10
* @property int $solo_top12
* @property int $solo_top25
* @property int $duo_kills
* @property int $duo_matchesplayed
* @property int $duo_score
* @property int $duo_minutesplayed
* @property Carbon $duo_lastmodified
* @property int $duo_top1
* @property int $duo_top3
* @property int $duo_top5
* @property int $duo_top6
* @property int $duo_top10
* @property int $duo_top12
* @property int $duo_top25
* @property int $squad_kills
* @property int $squad_matchesplayed
* @property int $squad_score
* @property int $squad_minutesplayed
* @property Carbon $squad_lastmodified
* @property int $squad_top1
* @property int $squad_top3
* @property int $squad_top5
* @property int $squad_top6
* @property int $squad_top10
* @property int $squad_top12
* @property int $squad_top25
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Account $account
* @property User $user
*/
class Stats extends Model
{
......@@ -34,6 +81,21 @@ class Stats extends Model
// Accessors & Mutators
//---------------------------------------------------------------------------------
public function setSquadLastmodifiedAttribute($value)
{
$this->attributes['squad_lastmodified'] = Carbon::createFromTimestampUTC($value);
}
public function setDuoLastmodifiedAttribute($value)
{
$this->attributes['duo_lastmodified'] = Carbon::createFromTimestampUTC($value);
}
public function setSoloLastmodifiedAttribute($value)
{
$this->attributes['solo_lastmodified'] = Carbon::createFromTimestampUTC($value);
}
//---------------------------------------------------------------------------------
// Public Methods
//---------------------------------------------------------------------------------
......@@ -42,4 +104,9 @@ class Stats extends Model
{
return $this->belongsTo('Onyx\Account');
}
public function user()
{
return $this->belongsTo(User::class);
}
}
......@@ -122,7 +122,7 @@ class CustomValidator extends Validator
$client = new FortniteClient();
try {
$account = $client->getAccountByTag($value, '');
$account = $client->getAccountByTag($value, $this->data['platform']);
} catch (FortniteApiNetworkException $ex) {
return false;
}
......
......@@ -8,6 +8,7 @@ use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Model;
use Onyx\Fortnite\Objects\Stats;
/**
* Class User.
......@@ -80,4 +81,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
{
return $this->hasOne('Onyx\Account', 'id', 'account_id');
}
/**
* @return Stats
*/
public function fortnite()
{
return $this->hasOne(Stats::class, 'user_id', 'id');
}
}
......@@ -48,4 +48,20 @@ abstract class Console
throw new \Exception('Unknown ID - '.$id);
}
}
/**
* @param int $id
* @return string
* @throws \Exception
*/
public static function getFortniteTag(int $id)
{
switch ($id) {
case self::Xbox:
return 'xb1';
default:
return self::getOverwatchTag($id);
}
}
}
......@@ -93,9 +93,13 @@ class AccountController extends Controller
$client = new FortniteClient();
$gamertag = $request->request->get('gamertag');
$platform = $request->request->get('platform');
/** @var Account $account */
[$id, $account] = $client->getAccountByTag($gamertag, '');
[$id, $account] = $client->getAccountByTag($gamertag, $platform);
// Update Stats
$client->getAccountRoyaleStats($account, $id);
return \Redirect::action('Fortnite\ProfileController@index', [$id]);
} catch (\Exception $ex) {
......
......@@ -4,6 +4,7 @@ namespace PandaLove\Http\Controllers\Fortnite;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Onyx\Fortnite\Objects\Stats;
use Onyx\XboxLive\Enums\Console;
use PandaLove\Http\Controllers\Controller;
......@@ -26,7 +27,12 @@ class ProfileController extends Controller
public function index(string $id)
{
try {
//
$stats = Stats::where('epic_id', $id)->firstOrFail();
return view('fortnite.profile', [
'account' => $stats->account,
'stats' => $stats
]);
} catch (ModelNotFoundException $e) {
\App::abort(404, 'We could not find this Fortnite Profile.');
}
......
......@@ -31,7 +31,7 @@ class AddFortniteRequest extends Request
return [
'gamertag' => 'required|min:3|fortnite-real',
//'platform' => 'required|in:0,1,2',
'platform' => 'required|in:0,1,2',
];
}
}
......@@ -16,6 +16,7 @@ class AddFortniteTables extends Migration
$table->increments('id');
$table->string('epic_id', 128);
$table->integer('account_id', false, true)->nullable();
$table->integer('user_id', false, true)->nullable();
$this->types($table, 'solo');
$this->places($table, 'solo');
......@@ -28,6 +29,7 @@ class AddFortniteTables extends Migration
$table->timestamps();
$table->foreign('account_id')->references('id')->on('accounts');
$table->foreign('user_id')->references('id')->on('users');
});
}
......@@ -43,8 +45,9 @@ class AddFortniteTables extends Migration
private function types(Blueprint $table, string $type)
{
$table->timestamp($type . '_lastmodified');
$table->integer($type . '_kills', false, true);
$table->integer($type . '_matches', false, true);
$table->integer($type . '_matchesplayed', false, true);
$table->integer($type . '_score', false, true);
$table->integer($type . '_minutesplayed', false, true);
}
......@@ -54,7 +57,9 @@ class AddFortniteTables extends Migration
$table->integer($type . '_top1', false, true);
$table->integer($type . '_top3', false, true);
$table->integer($type . '_top5', false, true);
$table->integer($type . '_top6', false, true);
$table->integer($type . '_top10', false, true);
$table->integer($type . '_top12', false, true);
$table->integer($type . '_top25', false, true);
}
}
......@@ -38,6 +38,7 @@ return [
'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.',
'filled' => 'The :attribute field is required.',
'fortnite_real' => 'The :attribute was not found on the selected platform',
'exists' => 'The selected :attribute is invalid.',
'gamertag_exists' => 'The :attribute is not in our system to be validated.',
'gamertag_real' => 'The :attribute is not a real gamertag.',
......
<?php
/** @var /Onyx/Account $account */
?>
@extends('app')
@section('content')
<div class="wrapper style1">
<article class="container no-image" id="top">
<div class="row">
<div class="12u">
<header>
<h1>Hi. I am <strong>{{ $account->gamertag }}</strong>
<small>({{ $account->console() }})</small>
</h1>
</header>
<div class="ui stackable menu">
<a class="active item" data-tab="overview">
Overview
</a>
<a class="item" data-tab="solo">
Solo
</a>
<a class="item" data-tab="duo">
Duo
</a>
<a class="item" data-tab="squad">
Squad
</a>
</div>
<div class="ui bottom attached active tab" data-tab="overview">
@include('includes.fortnite.profile.tabs.overview')
</div>
<div class="ui bottom attached tab" data-tab="solo">
@include('includes.fortnite.profile.tabs.solo')
</div>
<div class="ui bottom attached tab" data-tab="duo">
@include('includes.fortnite.profile.tabs.duo')
</div>
<div class="ui bottom attached tab" data-tab="squad">
@include('includes.fortnite.profile.tabs.squad')
</div>
</div>
</div>
</article>
</div>
@endsection
@section('inline-js')
<script type="text/javascript">
$('.menu .item').tab();
</script>
@append
@section('inline-css')
<style type="text/css">
</style>
@append
\ No newline at end of file
......@@ -4,12 +4,35 @@
@endforeach
<div class="two fields">
<div class="field {{ $errors->fortnite->has('gamertag') ? 'error' : '' }}">
<label>Epic Username</label>
<label><a target="_blank" href="https://www.epicgames.com/account/connected">Epic</a> Username</label>
<input type="text" name="gamertag" id="gamertag" placeholder="EPIC Username" />
</div>
<div class="field {{ $errors->fortnite->has('platform') ? 'error' : '' }}">
<label>Platform</label>
<div class="ui selection dropdown">
<input type="hidden" name="platform">
<i class="dropdown icon"></i>
<div class="default text">Platform</div>
<div class="menu">
<div class="item" data-value="{{ \Onyx\XboxLive\Enums\Console::Xbox }}">Xbox</div>
<div class="item" data-value="{{ \Onyx\XboxLive\Enums\Console::PSN }}">PSN</div>
<div class="item" data-value="{{ \Onyx\XboxLive\Enums\Console::PC }}">PC</div>
</div>
</div>
</div>
</div>
<br />
<ul class="actions">
<li><input type="submit" value="Add Fortnite Account" /></li>
</ul>
{!! Form::close() !!}
\ No newline at end of file
{!! Form::close() !!}
@section('inline-js')
<script type="text/javascript">
$(document).on('ready', function() {
$('.ui.dropdown')
.dropdown()
;
});
</script>
@append
\ No newline at end of file
<div class="ui attached segment">
<div class="row">
<div class="4u">
<div class="ui cards">
<div class="ui card">
<div class="image">
<img src="{{ asset('images/fortnite-default.png') }}" />
</div>
<div class="content">
<a class="header">{{ $account->gamertag }}</a>
<div class="meta">
<span class=""></span>
</div>
</div>
</div>
</div>
</div>
<div class="8u">
<div class="ui three statistics">
<div class="{{ $stats->solo_top1 > 0 ? 'green' : 'red' }} statistic">
<div class="value">
{{ $stats->solo_top1 }}
</div>
<div class="label">
Solo Wins
</div>
</div>
<div class="{{ $stats->duor_top1 > 0 ? 'green' : 'red' }} statistic">
<div class="value">
{{ $stats->duo_top1 }}
</div>
<div class="label">
Duo Wins
</div>
</div>
<div class="{{ $stats->squad_top1 > 0 ? 'green' : 'red' }} statistic">
<div class="value">
{{ $stats->squad_top1 }}
</div>
<div class="label">
Squad Wins
</div>
</div>
</div>
<div class="ui three statistics">
<div class="blue statistic">
<div class="value">
{{ $stats->solo_matchesplayed }}
</div>
<div class="label">
Solo Matches
</div>
</div>
<div class="blue statistic">
<div class="value">
{{ $stats->duo_matchesplayed }}
</div>
<div class="label">
Duo Matches
</div>
</div>
<div class="blue statistic">
<div class="value">
{{ $stats->squad_matchesplayed }}
</div>
<div class="label">
Squad Matches
</div>
</div>
</div>
<div class="ui icon message" id="update-message">
<i class="notched circle loading icon"></i>
<div class="content">
<div class="header">
Just one second
</div>
<p>
Checking if this profile needs an update.
</p>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment