├── .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 | []()
5 | []()
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 | }
--------------------------------------------------------------------------------