├── .travis.yml ├── src ├── Exception │ ├── InvalidStatException.php │ ├── UserNotFoundException.php │ ├── InvalidGameModeException.php │ ├── StatsNotFoundException.php │ ├── LeaderboardNotFoundException.php │ └── TwoFactorAuthRequiredException.php ├── Mode.php ├── Language.php ├── Platform.php ├── Model │ ├── FortniteLeaderboard.php │ ├── FortniteNews.php │ ├── Items.php │ └── FortniteStats.php ├── Status.php ├── Store.php ├── Challenges.php ├── Profile.php ├── News.php ├── Account.php ├── Leaderboard.php ├── Stats.php ├── Auth.php └── FortniteClient.php ├── phpunit.xml ├── .gitattributes ├── composer.json ├── .gitignore ├── LICENSE └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.0' 4 | - '7.1' 5 | script: phpunit --configuration phpunit.xml -------------------------------------------------------------------------------- /src/Exception/InvalidStatException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Language.php: -------------------------------------------------------------------------------- 1 | challenge = $challenge; 11 | } 12 | 13 | public function getChallenge() 14 | { 15 | return $this->challenge; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tustin/fortnite-php", 3 | "license": "MIT", 4 | "description": "PHP wrapper for the official Fortnite API.", 5 | "homepage": "https://github.com/Tustin/fortnite-php", 6 | "keywords": ["fortnite", "api", "php"], 7 | "autoload": { 8 | "classmap": [ 9 | "src/" 10 | ] 11 | }, 12 | "require": { 13 | "guzzlehttp/guzzle": "^6.3", 14 | "php": "^7.0" 15 | }, 16 | "scripts": { 17 | "test": "php tests/auth.php" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local files 2 | test.php 3 | 4 | # Windows image file caches 5 | Thumbs.db 6 | ehthumbs.db 7 | 8 | # Folder config file 9 | Desktop.ini 10 | 11 | # Recycle Bin used on file shares 12 | $RECYCLE.BIN/ 13 | 14 | # Windows Installer files 15 | *.cab 16 | *.msi 17 | *.msm 18 | *.msp 19 | 20 | # Windows shortcuts 21 | *.lnk 22 | 23 | # ========================= 24 | # Operating System Files 25 | # ========================= 26 | 27 | # OSX 28 | # ========================= 29 | 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Thumbnails 35 | ._* 36 | 37 | # Files that might appear in the root of a volume 38 | .DocumentRevisions-V100 39 | .fseventsd 40 | .Spotlight-V100 41 | .TemporaryItems 42 | .Trashes 43 | .VolumeIcon.icns 44 | 45 | # Directories potentially created on remote AFP share 46 | .AppleDB 47 | .AppleDesktop 48 | Network Trash Folder 49 | Temporary Items 50 | .apdisk 51 | 52 | vendor/ 53 | composer.lock 54 | tests/ -------------------------------------------------------------------------------- /src/Platform.php: -------------------------------------------------------------------------------- 1 | $mode) { 18 | switch ($key) { 19 | case "p2": 20 | $this->solo = new FortniteStats($mode); 21 | break; 22 | case "p9": 23 | $this->squad = new FortniteStats($mode); 24 | break; 25 | case "p10": 26 | $this->duo = new FortniteStats($mode); 27 | break; 28 | default: 29 | throw new InvalidGameModeException('Mode ' . $key . ' is invalid.'); // Will be thrown if a new game mode is added 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tustin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Model/FortniteLeaderboard.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | switch ($key) { 19 | case "rank": 20 | $this->rank = $value; 21 | break; 22 | case "accountId": 23 | $this->accountid = $value; 24 | break; 25 | case "value": 26 | $this->score = $value; 27 | break; 28 | case "displayName": 29 | $this->displayname = $value; 30 | break; 31 | default: 32 | throw new InvalidStatException('Leaderboard key '. $key . ' is not supported'); 33 | } 34 | } 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Model/FortniteNews.php: -------------------------------------------------------------------------------- 1 | $value) { 19 | 20 | switch ($key) { 21 | case "image": 22 | $this->image = $value; 23 | break; 24 | case "hidden": 25 | $this->hidden = $value; 26 | break; 27 | case "title": 28 | $this->title = html_entity_decode($value); 29 | break; 30 | case "body": 31 | $this->body = html_entity_decode($value); 32 | break; 33 | case "spotlight": 34 | $this->spotlight = $value; 35 | break; 36 | case "_type": 37 | break; 38 | default: 39 | throw new \Exception('News name ' . $key . ' is not supported'); 40 | } 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Status.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 16 | } 17 | 18 | public function get() 19 | { 20 | try { 21 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_STATUS_API . 'service/bulk/status?serviceId=Fortnite', 22 | $this->access_token); 23 | 24 | return $data[0]->status; 25 | } catch (GuzzleException $e) { 26 | if ($e->getResponse()->getStatusCode() == 404) throw new \Exception('Unable to obtain Fortnite status.'); 27 | throw $e; 28 | } 29 | } 30 | 31 | public function allowedToPlay(){ 32 | try { 33 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_STATUS_API . 'service/bulk/status?serviceId=Fortnite', 34 | $this->access_token); 35 | 36 | return !empty($data[0]->allowedActions) && in_array('PLAY',$data[0]->allowedActions); 37 | } catch (GuzzleException $e) { 38 | if ($e->getResponse()->getStatusCode() == 404) throw new \Exception('Unable to obtain Fortnite status.'); 39 | throw $e; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Store.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 15 | } 16 | 17 | public function get($lang = Language::ENGLISH) 18 | { 19 | if ($lang === Language::CHINESE && $lang === Language::JAPANESE) 20 | throw new \Exception("The language you're trying to use is currently not supported in the Fortnite store"); 21 | 22 | // TODO: cleanup 23 | if ($lang != Language::ENGLISH 24 | && $lang !== Language::GERMAN 25 | && $lang !== Language::SPANISH 26 | && $lang !== Language::FRENCH 27 | && $lang !== Language::FRENCH 28 | && $lang !== Language::ITALIAN 29 | && $lang !== Language::JAPANESE) 30 | throw new \Exception("Unknown Language"); 31 | 32 | try { 33 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_API . 'storefront/v2/catalog', 34 | $this->access_token, ['X-EpicGames-Language' => $lang]); 35 | return $data; 36 | } catch (GuzzleException $e) { 37 | if ($e->getResponse()->getStatusCode() == 404) throw new \Exception('Unable to obtain store info.'); 38 | throw $e; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Challenges.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 15 | $this->quests = $this->parseQuests((array)$profile_data); 16 | } 17 | 18 | public function getWeekly(int $week) { 19 | return array_filter($this->quests, function($value) use($week) { 20 | $padded_week = str_pad($week, 3, '0', STR_PAD_LEFT); 21 | return preg_match("/^questbundle_s(\d+)_week_$padded_week/", $value); 22 | }, ARRAY_FILTER_USE_KEY); 23 | } 24 | 25 | public function getWeeklys() { 26 | return array_filter($this->quests, function($value) { 27 | return preg_match("/^questbundle_s(\d+)_week/", $value); 28 | }, ARRAY_FILTER_USE_KEY); 29 | } 30 | 31 | private function parseQuests($items) { 32 | $quests = []; 33 | 34 | foreach ($items as $key => $item) { 35 | if (strpos($item->templateId, "ChallengeBundle:") === false) continue; 36 | $quest_bundle = explode(":", $item->templateId)[1]; 37 | $quests[$quest_bundle] = []; 38 | // Each ChallengeBundle "item" has an array of all the quests associated with it. 39 | foreach ($item->attributes->grantedquestinstanceids as $quest_id) { 40 | $quest = $items[$quest_id]; 41 | if (!$quest) continue; 42 | $quests[$quest_bundle][] = $quest; 43 | } 44 | } 45 | 46 | return $quests; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Profile.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 23 | $this->account_id = $account_id; 24 | $data = $this->fetch(); 25 | $this->items = new Items($data->items); 26 | $this->stats = new Stats($access_token, $account_id); 27 | $this->challenges = new Challenges($this->access_token, $data->items); 28 | 29 | } 30 | 31 | /** 32 | * Fetches profile data. 33 | * @return object Profile data 34 | */ 35 | private function fetch() { 36 | $data = FortniteClient::sendFortnitePostRequest(FortniteClient::FORTNITE_API . 'game/v2/profile/' . $this->account_id . '/client/QueryProfile?profileId=athena&rvn=-1', 37 | $this->access_token, 38 | new \StdClass()); 39 | return $data->profileChanges[0]->profile; 40 | } 41 | 42 | /** 43 | * Get current user's friends on Unreal Engine. 44 | * @return array Array of friends 45 | */ 46 | public function getFriends() { 47 | $data = FortniteClient::sendUnrealClientGetRequest(FortniteClient::EPIC_FRIENDS_ENDPOINT . $this->account_id, $this->access_token, true); 48 | 49 | return $data; 50 | } 51 | } -------------------------------------------------------------------------------- /src/Model/Items.php: -------------------------------------------------------------------------------- 1 | items = $this->parseItems((array)$items); 13 | } 14 | 15 | /** 16 | * Returns item by it's item id. 17 | * @param string $id Item id 18 | * @return object The item (null if not found) 19 | */ 20 | public function id($id) { 21 | foreach ($this->items as $item) { 22 | if ($item->itemId == $id) return $item; 23 | } 24 | 25 | return null; 26 | } 27 | 28 | /** 29 | * Returns all owned items. 30 | * @return array The items 31 | */ 32 | public function all() { 33 | return $this->items; 34 | } 35 | 36 | // 37 | // TODO (Tustin): maybe get all items of a certain type? Not really possible for me since I don't own more than like 5 items and they're all dances and gliders. 38 | // You would just need to parse 'templateId' for the first part to get the type. 39 | // 40 | 41 | /** 42 | * Parses a list of items and removes any non items (for some reason, quests show up in here) 43 | * @param array $items Items 44 | * @return array Actual items 45 | */ 46 | private function parseItems($items) { 47 | $actual = []; 48 | foreach ($items as $key => $item) { 49 | if (strpos($item->templateId, "Quest:") !== false) continue; 50 | $newItem = $item; 51 | $newItem->itemId = $key; // Add the itemId as a kvp since it only exists as the object identifier initially 52 | $actual[] = $newItem; 53 | } 54 | return $actual; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/News.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 21 | } 22 | 23 | public function get($type, $lang = Language::ENGLISH) 24 | { 25 | if ($lang !== Language::ENGLISH 26 | && $lang !== Language::GERMAN 27 | && $lang !== Language::SPANISH 28 | && $lang !== Language::CHINESE 29 | && $lang !== Language::FRENCH 30 | && $lang !== Language::FRENCH 31 | && $lang !== Language::ITALIAN 32 | && $lang !== Language::JAPANESE) 33 | throw new \Exception("Unknown Language"); 34 | 35 | if ($type != Self::SAVETHEWORLD && $type != Self::BATTLEROYALE) 36 | throw new \Exception("Only SaveTheWorld and BattleRoyale news are currently supported"); 37 | 38 | try { 39 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_NEWS_API . 'pages/fortnite-game', 40 | $this->access_token, ['Accept-Language' => $lang]); 41 | 42 | $data = $data->$type->news->messages; 43 | 44 | $news = []; 45 | foreach ($data as $key => $stat) { 46 | $news[$key] = new FortniteNews($stat); 47 | } 48 | 49 | return $news; 50 | } catch (GuzzleException $e) { 51 | if ($e->getResponse()->getStatusCode() == 404) throw new \Exception('Unable to obtain news.'); 52 | throw $e; 53 | } 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fortnite-PHP Wrapper 2 | Interact with the official Fortnite API using PHP. 3 | 4 | [![Packagist](https://img.shields.io/packagist/l/doctrine/orm.svg)]() 5 | [![Packagist](https://img.shields.io/packagist/v/Tustin/fortnite-php.svg)]() 6 | 7 | ## Installation 8 | Pull in the project using composer: 9 | `composer require Tustin/fortnite-php` 10 | 11 | ## Usage 12 | Create a basic test script to ensure everything was installed properly 13 | ```php 14 | profile->stats); 29 | 30 | // Grab someone's stats 31 | $sandy = $auth->profile->stats->lookup('sandalzrevenge'); 32 | echo 'Sandy Ravage has won ' . $sandy->pc->solo->wins . ' solo games and ' . $sandy->pc->squad->wins . ' squad games!'; 33 | ``` 34 | 35 | ### Get Leaderboards 36 | ```php 37 | $auth = Auth::login('epic_email@domain.com','password'); 38 | var_dump($auth->leaderboard->get(Platform::PC, Mode::DUO)); 39 | 40 | ``` 41 | 42 | ### Get News 43 | ```php 44 | $auth = Auth::login('epic_email@domain.com','password'); 45 | var_dump($auth->news->get(News::BATTLEROYALE, Language::ENGLISH)); 46 | ``` 47 | 48 | 49 | 50 | ### Get Store 51 | ```php 52 | $auth = Auth::login('epic_email@domain.com','password'); 53 | var_dump($auth->store->get(Language::ENGLISH)); 54 | ``` 55 | 56 | ### Get Challenges 57 | ```php 58 | $auth = Auth::login('epic_email@domain.com','password'); 59 | // All weekly challenges 60 | var_dump($auth->profile->challenges->getWeeklys()); 61 | 62 | // Or just get a specific week (in this example, week 1) 63 | var_dump($auth->profile->challenges->getWeekly(1)); 64 | ``` 65 | 66 | ### Constants 67 | ``` 68 | Platform [ PC, PS4, XB1 ] 69 | 70 | Mode [ SOLO, DUO, SQUAD ] 71 | 72 | Language [ ENGLISH, GERMAN, SPANISH, CHINESE, FRENCH, ITALIAN, JAPANESE ] 73 | 74 | News [ BATTLEROYALE, SAVETHEWORLD ] 75 | ``` 76 | 77 | ## Contributing 78 | Fortnite now utilizes SSL certificate pinning in their Windows client in newer versions. I suggest using the iOS mobile app to do any future API reversing as both cheat protections on the Windows client make it difficult to remove the certificate pinning. If SSL certificate pinning is added to the iOS version, I could easily provide a patch to remove that as the iOS version doesn't contain any anti-cheat. 79 | -------------------------------------------------------------------------------- /src/Account.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 17 | $this->account_id = $account_id; 18 | } 19 | 20 | public static function getDisplayNameFromID($id, $access_token) { 21 | try { 22 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_ACCOUNT_API . "public/account?accountId={$id}", 23 | $access_token); 24 | 25 | return $data[0]->displayName; 26 | } catch (GuzzleException $e) { 27 | if ($e->getResponse()->getStatusCode() == 404) throw new Exception('Could not get display name of account id ' . $id); 28 | throw $e; 29 | } 30 | } 31 | 32 | public function getDisplayNamesFromID($id) { 33 | try { 34 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_ACCOUNT_API . "public/account?accountId=" . join('&accountId=', $id), 35 | $this->access_token); 36 | 37 | return $data; 38 | } catch (GuzzleException $e) { 39 | if ($e->getResponse()->getStatusCode() == 404) throw new \Exception('Could not get display name of account id ' . $id); 40 | throw $e; 41 | } 42 | } 43 | 44 | public function killSession() { 45 | FortniteClient::sendFortniteDeleteRequest(FortniteClient::FORTNITE_ACCOUNT_API . "oauth/sessions/kill/" . $this->access_token, $this->access_token); 46 | } 47 | 48 | public function acceptEULA(){ 49 | try { 50 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_EULA_API . "public/agreements/fn/account/" . $this->account_id .'?locale=en-US', 51 | $this->access_token); 52 | 53 | FortniteClient::sendFortnitePostRequest(FortniteClient::FORTNITE_EULA_API . "public/agreements/fn/version/".$data->version."/account/".$this->account_id."/accept?locale=en", 54 | $this->access_token,new \StdClass()); 55 | 56 | FortniteClient::sendFortnitePostRequest(FortniteClient::FORTNITE_API.'game/v2/grant_access/'.$this->account_id, 57 | $this->access_token,new \StdClass()); 58 | 59 | return true; 60 | } catch (GuzzleException $e) { 61 | if ($e->getResponse()->getStatusCode() == 404) throw new \Exception('Could not read or accept EULA for account id ' . $this->account_id); 62 | throw $e; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/Model/FortniteStats.php: -------------------------------------------------------------------------------- 1 | $value) { 33 | switch ($key) { 34 | case "placetop1": 35 | $this->wins = $value; 36 | break; 37 | case "placetop3": 38 | $this->top3 = $value; 39 | break; 40 | case "placetop5": 41 | $this->top5 = $value; 42 | break; 43 | case "placetop6": 44 | $this->top6 = $value; 45 | break; 46 | case "placetop10": 47 | $this->top10 = $value; 48 | break; 49 | case "placetop12": 50 | $this->top12 = $value; 51 | break; 52 | case "placetop25": 53 | $this->top25 = $value; 54 | break; 55 | case "matchesplayed": 56 | $this->matches_played = $value; 57 | break; 58 | case "kills": 59 | $this->kills = $value; 60 | break; 61 | case "score": 62 | $this->score = $value; 63 | break; 64 | case "minutesplayed": 65 | $this->minutes_played = $value; 66 | break; 67 | case "lastmodified": 68 | $this->last_modified = $value; 69 | break; 70 | default: 71 | throw new InvalidStatException('Stat name '. $key . ' is not supported'); // I expect a PR if someone runs into this exception 72 | } 73 | } 74 | 75 | // TODO: Cleanup 76 | $this->losses = ($this->matches_played === 0) ? 0 : $this->matches_played - $this->wins; 77 | $this->kills_per_match = ($this->matches_played === 0) ? 0 : round($this->kills / $this->matches_played, 2); 78 | $this->score_per_match = ($this->matches_played === 0) ? 0 : round($this->score / $this->matches_played, 2); 79 | $this->kill_death_ratio = ($this->matches_played - $this->wins === 0) ? 0 : round($this->kills / ($this->matches_played - $this->wins), 2); 80 | $this->wins_ratio = ($this->matches_played === 0) ? 0 : round(100 * $this->wins / ($this->wins + $this->losses), 2); 81 | } 82 | } -------------------------------------------------------------------------------- /src/Leaderboard.php: -------------------------------------------------------------------------------- 1 | account = $account; 26 | $this->in_app_id = $in_app_id; 27 | $this->access_token = $access_token; 28 | } 29 | 30 | /** 31 | * Get leaderboard (top 50) 32 | * @param string $platform (PC, PS4, XB1) 33 | * @param string $type (SOLO,DUO, SQUAD) 34 | * @return object New instance of Fortnite\Leaderboard 35 | */ 36 | public function get($platform, $type) 37 | { 38 | if ($platform !== Platform::PC 39 | && $platform !== Platform::PS4 40 | && $platform !== Platform::XBOX1) 41 | throw new \Exception('Please select a platform'); 42 | 43 | if ($type !== Mode::DUO 44 | && $type !== Mode::SOLO 45 | && $type !== Mode::SQUAD) { 46 | throw new \Exception('Please select a game mode'); 47 | } 48 | 49 | try { 50 | $data_cohort = FortniteClient::sendFortniteGetRequest( 51 | FortniteClient::FORTNITE_API . "game/v2/leaderboards/cohort/$this->in_app_id?playlist={$platform}_m0{$type}", 52 | $this->access_token, array('Content-Type: application/json') 53 | ); 54 | 55 | 56 | 57 | $data = FortniteClient::sendFortnitePostRequest( 58 | FortniteClient::FORTNITE_API . "leaderboards/type/global/stat/br_placetop1_{$platform}_m0{$type}/window/weekly?ownertype=1&itemsPerPage=50", 59 | $this->access_token, $data_cohort->cohortAccounts 60 | ); 61 | $entries = $data->entries; 62 | 63 | 64 | $ids = array(); 65 | foreach ($entries as $entry) { 66 | $entry->accountId = str_replace("-", "", $entry->accountId); 67 | array_push($ids, $entry->accountId); 68 | } 69 | 70 | $accounts = $this->account->getDisplayNamesFromID($ids); 71 | 72 | foreach ($entries as $entry) { 73 | foreach ($accounts as $account) { 74 | if ($entry->accountId === $account->id) { 75 | $entry->displayName = $account->displayName ?? null; 76 | break; 77 | } 78 | } 79 | } 80 | 81 | $leaderboard = []; 82 | foreach ($entries as $key => $stat) { 83 | $leaderboard[$key] = new FortniteLeaderboard($stat); 84 | } 85 | 86 | return $leaderboard; 87 | } catch (GuzzleException $e) { 88 | if ($e->getResponse()->getStatusCode() == 404) { 89 | throw new LeaderboardNotFoundException('Could not get leaderboards.'); 90 | } 91 | throw $e; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/Stats.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 30 | $this->account_id = $account_id; 31 | $data = $this->fetch($this->account_id); 32 | if (array_key_exists("ps4", $data)) $this->ps4 = $data["ps4"]; 33 | if (array_key_exists("pc", $data)) $this->pc = $data["pc"]; 34 | if (array_key_exists("xb1", $data)) $this->xb1 = $data["xb1"]; 35 | } 36 | 37 | /** 38 | * Fetches stats for the current user. 39 | * @param string $account_id Account id 40 | * @return object The stats data 41 | */ 42 | private function fetch($account_id) { 43 | if (!$account_id) return null; 44 | 45 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_API . 'stats/accountId/' . $account_id . '/bulk/window/alltime', 46 | $this->access_token); 47 | 48 | // Remove - from account ID and get it's display name 49 | $this->display_name = Account::getDisplayNameFromID(str_replace("-","",$this->account_id), $this->access_token); 50 | //if (!count($data)) throw new StatsNotFoundException('Unable to find any stats for account id '. $account_id); 51 | 52 | // Loop over all the stat objects and compile them together cleanly 53 | $compiledStats = []; 54 | foreach ($data as $stat) { 55 | $parsed = $this->parseStatItem($stat); 56 | $compiledStats = array_merge_recursive($compiledStats, $parsed); 57 | } 58 | 59 | // Now loop over the compiled stats and create proper objects 60 | $platforms = []; 61 | foreach ($compiledStats as $key => $platform) { 62 | $platforms[$key] = new Platform($platform); 63 | } 64 | 65 | return $platforms; 66 | } 67 | 68 | /** 69 | * Lookup a user by their Epic display name. 70 | * @param string $username Display name to search 71 | * @return object New instance of Fortnite\Stats 72 | */ 73 | public function lookup($username) { 74 | try { 75 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_PERSONA_API . 'public/account/lookup?q=' . urlencode($username), 76 | $this->access_token); 77 | return new self($this->access_token, $data->id); 78 | } catch (GuzzleException $e) { 79 | if ($e->getResponse()->getStatusCode() == 404) throw new UserNotFoundException('User ' . $username . ' was not found.'); 80 | throw $e; //If we didn't get the user not found status code, just re-throw the error. 81 | } 82 | } 83 | 84 | //TODO (Tustin): Make this not redundant 85 | public static function lookupWithToken($username, $access_token) { 86 | try { 87 | $data = FortniteClient::sendFortniteGetRequest(FortniteClient::FORTNITE_PERSONA_API . 'public/account/lookup?q=' . urlencode($username), 88 | $access_token); 89 | return new self($access_token, $data->id); 90 | } catch (GuzzleException $e) { 91 | if ($e->getResponse()->getStatusCode() == 404) throw new UserNotFoundException('User ' . $username . ' was not found.'); 92 | throw $e; //If we didn't get the user not found status code, just re-throw the error. 93 | } 94 | } 95 | 96 | /** 97 | * Parses a stat string into a mapped array. 98 | * @param string $stat The stat string 99 | * @return array The mapped stat array 100 | */ 101 | private function parseStatItem($stat): array { 102 | // 103 | // Example stat name: 104 | // br_placetop5_ps4_m0_p10 105 | // {type}_{name}_{platform}_{??}_{mode (squads/solo/duo)} 106 | // 107 | $result = []; 108 | $pieces = explode("_", $stat->name); 109 | $result[$pieces[2]][$pieces[4]][$pieces[1]] = $stat->value; 110 | return $result; 111 | } 112 | 113 | public function accountId() { 114 | return $this->account_id; 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /src/Auth.php: -------------------------------------------------------------------------------- 1 | access_token = $access_token; 31 | $this->in_app_id = $in_app_id; 32 | $this->refresh_token = $refresh_token; 33 | $this->account_id = $account_id; 34 | $this->expires_in = $expires_in; 35 | $this->account = new Account($this->access_token,$this->account_id); 36 | $this->status = new Status($this->access_token); 37 | if ($this->status->allowedToPlay() === false){ 38 | $this->account->acceptEULA(); 39 | } 40 | $this->profile = new Profile($this->access_token, $this->account_id); 41 | $this->leaderboard = new Leaderboard($this->access_token, $this->in_app_id, $this->account); 42 | $this->store = new Store($this->access_token); 43 | $this->news = new News($this->access_token); 44 | } 45 | 46 | /** 47 | * Login using Unreal Engine credentials to access Fortnite API. 48 | * 49 | * @param string $email The account email 50 | * @param string $password The account password 51 | * 52 | * @throws Exception Throws exception on API response errors (might get overridden by Guzzle exceptions) 53 | * 54 | * @return self New Auth instance 55 | */ 56 | public static function login($email, $password, $challenge = '', $code = '') { 57 | 58 | $requestParams = [ 59 | 'includePerms' => 'false', // We don't need these here 60 | 'token_type' => 'eg1' 61 | ]; 62 | 63 | if (empty($challenge) && empty($code)) { 64 | // Regular login 65 | $requestParams = array_merge($requestParams, [ 66 | 'grant_type' => 'password', 67 | 'username' => $email, 68 | 'password' => $password, 69 | ]); 70 | } else { 71 | $requestParams = array_merge($requestParams, [ 72 | 'grant_type' => 'otp', 73 | 'otp' => $code, 74 | 'challenge' => $challenge, 75 | ]); 76 | } 77 | 78 | // First, we need to get a token for the Unreal Engine client 79 | $data = FortniteClient::sendUnrealClientPostRequest(FortniteClient::EPIC_OAUTH_TOKEN_ENDPOINT, $requestParams); 80 | 81 | if (!isset($data->access_token)) { 82 | if ($data->errorCode === 'errors.com.epicgames.common.two_factor_authentication.required') { 83 | throw new TwoFactorAuthRequiredException($data->challenge); 84 | } 85 | 86 | throw new \Exception($data->errorMessage); 87 | } 88 | 89 | // Now that we've got our Unreal Client launcher token, let's get an exchange token for Fortnite 90 | $data = FortniteClient::sendUnrealClientGetRequest(FortniteClient::EPIC_OAUTH_EXCHANGE_ENDPOINT, $data->access_token, true); 91 | 92 | if (!isset($data->code)) { 93 | throw new \Exception($data->errorMessage); 94 | } 95 | 96 | // Should be good. Let's get our tokens for the Fortnite API 97 | $data = FortniteClient::sendUnrealClientPostRequest(FortniteClient::EPIC_OAUTH_TOKEN_ENDPOINT, [ 98 | 'grant_type' => 'exchange_code', 99 | 'exchange_code' => $data->code, 100 | 'includePerms' => false, // We don't need these here 101 | 'token_type' => 'eg1' 102 | ], FortniteClient::FORTNITE_AUTHORIZATION); 103 | 104 | if (!isset($data->access_token)) { 105 | throw new \Exception($data->errorMessage); 106 | } 107 | 108 | return new self($data->access_token, $data->in_app_id, $data->refresh_token, $data->account_id, $data->expires_in); 109 | } 110 | 111 | /** 112 | * Refreshes OAuth2 tokens using an existing refresh token. 113 | * @param string $refresh_token Exisiting OAuth2 refresh token 114 | * @return self New Auth instance 115 | */ 116 | public static function refresh($refresh_token) { 117 | $data = FortniteClient::sendUnrealClientPostRequest(FortniteClient::EPIC_OAUTH_TOKEN_ENDPOINT, [ 118 | 'grant_type' => 'refresh_token', 119 | 'refresh_token' => $refresh_token, 120 | 'includePerms' => "false", // We don't need these here 121 | 'token_type' => 'eg1', 122 | ], FortniteClient::FORTNITE_AUTHORIZATION); 123 | 124 | if (!$data->access_token) { 125 | throw new \Exception($data->errorMessage); 126 | } 127 | 128 | return new self($data->access_token, $data->in_app_id, $data->refresh_token, $data->account_id, $data->expires_in); 129 | } 130 | 131 | /** 132 | * Returns current refresh token. 133 | * @return string OAuth2 refresh token 134 | */ 135 | public function refreshToken() { 136 | return $this->refresh_token; 137 | } 138 | 139 | /** 140 | * Returns the time until the OAuth2 access token expires. 141 | * @return string Time until OAuth2 access token expires (in seconds) 142 | */ 143 | public function expiresIn() { 144 | return $this->expires_in; 145 | } 146 | 147 | /** 148 | * Returns current access token. 149 | * @return string OAuth2 access token 150 | */ 151 | public function accessToken() { 152 | return $this->access_token; 153 | } 154 | 155 | public function inAppId() { 156 | return $this->in_app_id; 157 | } 158 | } -------------------------------------------------------------------------------- /src/FortniteClient.php: -------------------------------------------------------------------------------- 1 | get($endpoint, [ 58 | 'headers' => [ 59 | 'User-Agent' => self::UNREAL_CLIENT_USER_AGENT, 60 | 'Authorization' => (!$oauth) ? 'basic ' . $authorization : 'bearer ' . $authorization 61 | ] 62 | ]); 63 | 64 | return json_decode($response->getBody()->getContents()); 65 | } catch (GuzzleException $e) { 66 | throw $e; //Throw exception back up to caller 67 | } 68 | } 69 | 70 | /** 71 | * Sends a POST request as the Unreal Engine Client. 72 | * @param string $endpoint API Endpoint to request 73 | * @param array $params Request parameters, using the name as the array key and value as the array value 74 | * @param string $authorization Authorization header 75 | * @param boolean $oauth Is $authorization an OAuth2 token 76 | * @return object Decoded JSON response body 77 | */ 78 | public static function sendUnrealClientPostRequest($endpoint, $params = null, $authorization = self::EPIC_LAUNCHER_AUTHORIZATION, $oauth = false) { 79 | $client = new Client(['http_errors' => false]); 80 | 81 | try { 82 | $response = $client->post($endpoint, [ 83 | 'form_params' => $params, 84 | 'headers' => [ 85 | 'User-Agent' => self::UNREAL_CLIENT_USER_AGENT, 86 | 'Authorization' => (!$oauth) ? 'basic ' . $authorization : 'bearer ' . $authorization, 87 | 'X-Epic-Device-ID' => self::generateDeviceId() 88 | ] 89 | ]); 90 | 91 | return json_decode($response->getBody()->getContents()); 92 | } catch (GuzzleException $e) { 93 | throw $e; //Throw exception back up to caller 94 | } 95 | } 96 | 97 | /** 98 | * Sends a GET request as the Fortnite client. 99 | * @param string $endpoint API endpoint to request 100 | * @param string $access_token OAuth2 access token 101 | * @param array $extra_headers (optional) 102 | * @return object Decoded JSON response body 103 | */ 104 | public static function sendFortniteGetRequest($endpoint, $access_token, $extra_headers = array()) { 105 | $client = new Client(); 106 | 107 | $headers = [ 108 | 'User-Agent' => self::FORTNITE_USER_AGENT, 109 | 'Authorization' => 'bearer ' . $access_token 110 | ]; 111 | 112 | $headers = array_merge($headers, $extra_headers); 113 | try { 114 | $response = $client->get($endpoint, [ 115 | 'headers' => $headers 116 | ]); 117 | 118 | return json_decode($response->getBody()->getContents()); 119 | } catch (GuzzleException $e) { 120 | throw $e; //Throw exception back up to caller 121 | } 122 | } 123 | 124 | 125 | /** 126 | * Sends a POST request as the Fortnite client. 127 | * @param string $endpoint API endpoint to request 128 | * @param string $access_token OAuth2 access token 129 | * @param array $params Request parameters, using the name as the array key and value as the array value 130 | * @return object Decoded JSON response body 131 | */ 132 | public static function sendFortnitePostRequest($endpoint, $access_token, $params = null) { 133 | $client = new Client(); 134 | 135 | try { 136 | $response = $client->post($endpoint, [ 137 | 'json' => $params, 138 | 'headers' => [ 139 | 'User-Agent' => self::FORTNITE_USER_AGENT, 140 | 'Authorization' => 'bearer ' . $access_token 141 | ] 142 | ]); 143 | 144 | return json_decode($response->getBody()->getContents()); 145 | } catch (GuzzleException$e) { 146 | throw $e; //Throw exception back up to caller 147 | } 148 | 149 | } 150 | 151 | public static function sendFortniteDeleteRequest($endpoint, $access_token) { 152 | $client = new Client(); 153 | 154 | try { 155 | $response = $client->delete($endpoint, [ 156 | 'json' => $params, 157 | 'headers' => [ 158 | 'User-Agent' => self::FORTNITE_USER_AGENT, 159 | 'Authorization' => 'bearer ' . $access_token 160 | ] 161 | ]); 162 | 163 | return json_decode($response->getBody()->getContents()); 164 | } catch (GuzzleException$e) { 165 | throw $e; //Throw exception back up to caller 166 | } 167 | 168 | } 169 | 170 | private static function generateSequence($length) { 171 | return strtoupper((bin2hex(random_bytes($length / 2)))); 172 | } 173 | 174 | public static function generateDeviceId() { 175 | return sprintf('%s-%s-%s-%s-%s', 176 | self::generateSequence(8), 177 | self::generateSequence(4), 178 | self::generateSequence(4), 179 | self::generateSequence(4), 180 | self::generateSequence(12)); 181 | } 182 | } --------------------------------------------------------------------------------