@spinzaf/freefire-api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +297 -0
- package/config/settings.yaml +26 -0
- package/data/items.json +153075 -0
- package/index.js +3 -0
- package/lib/api.js +229 -0
- package/lib/constants.js +115 -0
- package/lib/crypto.js +15 -0
- package/lib/protobuf.js +51 -0
- package/lib/utils.js +112 -0
- package/package.json +45 -0
- package/proto/MajorLogin.proto +173 -0
- package/proto/PlayerCSStats.proto +43 -0
- package/proto/PlayerPersonalShow.proto +641 -0
- package/proto/PlayerStats.proto +38 -0
- package/proto/SearchAccountByName.proto +13 -0
- package/proto/SetPlayerGalleryShowInfo.proto +70 -0
- package/test/all.js +26 -0
- package/test/items.js +51 -0
- package/test/login.js +16 -0
- package/test/profile.js +40 -0
- package/test/search.js +24 -0
- package/test/stats.js +47 -0
package/index.js
ADDED
package/lib/api.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const protoHandler = require('./protobuf');
|
|
3
|
+
const { AE, HEADERS, URLS, GARENA_CLIENT, DEFAULT_CREDENTIALS } = require('./constants');
|
|
4
|
+
const { processPlayerItems } = require('./utils');
|
|
5
|
+
|
|
6
|
+
class FreeFireAPI {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.session = {
|
|
9
|
+
token: null,
|
|
10
|
+
serverUrl: null,
|
|
11
|
+
openId: null,
|
|
12
|
+
accountId: null
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Authenticate with Garena using UID and Password (Guest/Account)
|
|
18
|
+
* @param {string} [uid] - (Optional) User ID
|
|
19
|
+
* @param {string} [password] - (Optional) Password
|
|
20
|
+
*/
|
|
21
|
+
async login(uid = null, password = null) {
|
|
22
|
+
// Use default credentials if not provided
|
|
23
|
+
if (!uid || !password) {
|
|
24
|
+
console.log("[i] No credentials provided, loading from config/credentials.yaml.");
|
|
25
|
+
uid = DEFAULT_CREDENTIALS.UID;
|
|
26
|
+
password = DEFAULT_CREDENTIALS.PASSWORD;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!uid || !password) {
|
|
30
|
+
throw new Error("Missing credentials. Set UID and PASSWORD in config/credentials.yaml or pass them to login(uid, password).");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Step 1: Get Garena Token
|
|
34
|
+
const garenaData = await this._getGarenaToken(uid, password);
|
|
35
|
+
if (!garenaData || !garenaData.access_token) {
|
|
36
|
+
throw new Error("Garena authentication failed: Invalid credentials or response");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Step 2: Major Login
|
|
40
|
+
const loginData = await this._majorLogin(garenaData.access_token, garenaData.open_id);
|
|
41
|
+
if (!loginData || !loginData.token) {
|
|
42
|
+
throw new Error("Major login failed: Empty token received");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.session.token = loginData.token;
|
|
46
|
+
this.session.serverUrl = loginData.serverUrl;
|
|
47
|
+
this.session.openId = garenaData.open_id;
|
|
48
|
+
this.session.accountId = loginData.accountid;
|
|
49
|
+
|
|
50
|
+
return this.session;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async _getGarenaToken(uid, password) {
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
params.append('uid', uid);
|
|
56
|
+
params.append('password', password);
|
|
57
|
+
params.append('response_type', 'token');
|
|
58
|
+
params.append('client_type', '2');
|
|
59
|
+
params.append('client_secret', GARENA_CLIENT.CLIENT_SECRET);
|
|
60
|
+
params.append('client_id', GARENA_CLIENT.CLIENT_ID);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await axios.post(URLS.GARENA_TOKEN, params, {
|
|
64
|
+
headers: HEADERS.GARENA_AUTH
|
|
65
|
+
});
|
|
66
|
+
return response.data;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error(`Garena Auth Request Failed: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _majorLogin(accessToken, openId) {
|
|
73
|
+
const payload = {
|
|
74
|
+
openid: openId,
|
|
75
|
+
logintoken: accessToken,
|
|
76
|
+
platform: "4"
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const encryptedBody = await protoHandler.encode('MajorLogin.proto', 'request', payload, true);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await axios.post(URLS.MAJOR_LOGIN, encryptedBody, {
|
|
83
|
+
headers: {
|
|
84
|
+
...HEADERS.COMMON,
|
|
85
|
+
'Authorization': 'Bearer', // Specific to MajorLogin
|
|
86
|
+
'Content-Type': 'application/octet-stream'
|
|
87
|
+
},
|
|
88
|
+
responseType: 'arraybuffer'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return await protoHandler.decode('MajorLogin.proto', 'response', response.data);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(`Major Login Request Failed: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Search for accounts by name (fuzzy search)
|
|
99
|
+
* @param {string} keyword
|
|
100
|
+
* @returns {Promise<Array>} List of matching accounts
|
|
101
|
+
*/
|
|
102
|
+
async searchAccount(keyword) {
|
|
103
|
+
await this._checkSession();
|
|
104
|
+
|
|
105
|
+
if (keyword.length < 3) {
|
|
106
|
+
throw new Error("Search keyword must be at least 3 characters long.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const payload = { keyword: String(keyword) };
|
|
110
|
+
const encryptedBody = await protoHandler.encode('SearchAccountByName.proto', 'SearchAccountByName.request', payload, true);
|
|
111
|
+
|
|
112
|
+
const url = URLS.SEARCH(this.session.serverUrl);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await axios.post(url, encryptedBody, {
|
|
116
|
+
headers: {
|
|
117
|
+
...HEADERS.COMMON,
|
|
118
|
+
'Authorization': `Bearer ${this.session.token}`,
|
|
119
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
120
|
+
},
|
|
121
|
+
responseType: 'arraybuffer'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const data = await protoHandler.decode('SearchAccountByName.proto', 'SearchAccountByName.response', response.data);
|
|
125
|
+
return data.infos; // Field name is 'infos' in proto
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(`Search Failed: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get detailed player profile (Personal Show)
|
|
133
|
+
* @param {number|string} uid
|
|
134
|
+
* @returns {Promise<Object>} Player data including profile, guild, etc.
|
|
135
|
+
*/
|
|
136
|
+
async getPlayerProfile(uid) {
|
|
137
|
+
await this._checkSession();
|
|
138
|
+
|
|
139
|
+
const payload = {
|
|
140
|
+
accountId: Number(uid),
|
|
141
|
+
callSignSrc: 7,
|
|
142
|
+
needGalleryInfo: true
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const encryptedBody = await protoHandler.encode('PlayerPersonalShow.proto', 'request', payload, true);
|
|
146
|
+
const url = URLS.PERSONAL_SHOW(this.session.serverUrl);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await axios.post(url, encryptedBody, {
|
|
150
|
+
headers: {
|
|
151
|
+
...HEADERS.COMMON,
|
|
152
|
+
'Authorization': `Bearer ${this.session.token}`
|
|
153
|
+
},
|
|
154
|
+
responseType: 'arraybuffer'
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return await protoHandler.decode('PlayerPersonalShow.proto', 'response', response.data);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
throw new Error(`Get Profile Failed: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get player items (outfit, weapons, skills, pet)
|
|
165
|
+
* @param {number|string} uid
|
|
166
|
+
*/
|
|
167
|
+
async getPlayerItems(uid) {
|
|
168
|
+
const profile = await this.getPlayerProfile(uid);
|
|
169
|
+
if (!profile) return null;
|
|
170
|
+
return processPlayerItems(profile);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get Player Stats
|
|
175
|
+
* @param {number|string} uid
|
|
176
|
+
* @param {'br'|'cs'} mode - Battle Royale or Clash Squad
|
|
177
|
+
* @param {'career'|'ranked'|'normal'} matchType
|
|
178
|
+
*/
|
|
179
|
+
async getPlayerStats(uid, mode = 'br', matchType = 'career') {
|
|
180
|
+
await this._checkSession();
|
|
181
|
+
|
|
182
|
+
const modeLower = mode.toLowerCase();
|
|
183
|
+
const typeUpper = matchType.toUpperCase();
|
|
184
|
+
|
|
185
|
+
let matchMode = 0;
|
|
186
|
+
let url = '';
|
|
187
|
+
let protoFile = '';
|
|
188
|
+
let payload = { accountid: Number(uid) };
|
|
189
|
+
|
|
190
|
+
if (modeLower === 'br') {
|
|
191
|
+
const types = { 'CAREER': 0, 'NORMAL': 1, 'RANKED': 2 };
|
|
192
|
+
matchMode = types[typeUpper] !== undefined ? types[typeUpper] : 0;
|
|
193
|
+
url = URLS.PLAYER_STATS(this.session.serverUrl);
|
|
194
|
+
protoFile = 'PlayerStats.proto';
|
|
195
|
+
payload.matchmode = matchMode;
|
|
196
|
+
} else {
|
|
197
|
+
const types = { 'CAREER': 0, 'NORMAL': 1, 'RANKED': 6 };
|
|
198
|
+
matchMode = types[typeUpper] !== undefined ? types[typeUpper] : 0;
|
|
199
|
+
url = URLS.PLAYER_CS_STATS(this.session.serverUrl);
|
|
200
|
+
protoFile = 'PlayerCSStats.proto';
|
|
201
|
+
payload.gamemode = 15; // CS default
|
|
202
|
+
payload.matchmode = matchMode;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const encryptedBody = await protoHandler.encode(protoFile, 'request', payload, true);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const response = await axios.post(url, encryptedBody, {
|
|
209
|
+
headers: {
|
|
210
|
+
...HEADERS.COMMON,
|
|
211
|
+
'Authorization': `Bearer ${this.session.token}`
|
|
212
|
+
},
|
|
213
|
+
responseType: 'arraybuffer'
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return await protoHandler.decode(protoFile, 'response', response.data);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`Get Stats Failed: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// ----- Auto login if no session with Default Data.
|
|
222
|
+
async _checkSession() {
|
|
223
|
+
if (!this.session.token || !this.session.serverUrl) {
|
|
224
|
+
await this.login();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = FreeFireAPI;
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function parseSimpleYaml(content) {
|
|
5
|
+
const result = {};
|
|
6
|
+
const lines = content.split(/\r?\n/);
|
|
7
|
+
|
|
8
|
+
for (const rawLine of lines) {
|
|
9
|
+
const line = rawLine.trim();
|
|
10
|
+
if (!line || line.startsWith('#')) continue;
|
|
11
|
+
|
|
12
|
+
const separatorIndex = line.indexOf(':');
|
|
13
|
+
if (separatorIndex === -1) continue;
|
|
14
|
+
|
|
15
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
16
|
+
let value = line.slice(separatorIndex + 1).trim();
|
|
17
|
+
|
|
18
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
19
|
+
value = value.slice(1, -1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
result[key] = value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadYamlFile(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
const yamlRaw = fs.readFileSync(filePath, 'utf8');
|
|
31
|
+
return parseSimpleYaml(yamlRaw);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readConfigValue(config, key, fallback) {
|
|
38
|
+
const value = config[key];
|
|
39
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireConfigValue(config, key) {
|
|
44
|
+
const value = readConfigValue(config, key, '');
|
|
45
|
+
if (!value) {
|
|
46
|
+
throw new Error(`Missing required setting in config/settings.yaml: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadDefaultCredentials() {
|
|
52
|
+
const parsed = loadYamlFile(path.join(__dirname, '../config/credentials.yaml'));
|
|
53
|
+
return {
|
|
54
|
+
UID: readConfigValue(parsed, 'UID', ''),
|
|
55
|
+
PASSWORD: readConfigValue(parsed, 'PASSWORD', '')
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadSettings() {
|
|
60
|
+
const parsed = loadYamlFile(path.join(__dirname, '../config/settings.yaml'));
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
AE: {
|
|
64
|
+
MAIN_KEY: Buffer.from(requireConfigValue(parsed, 'AE_MAIN_KEY'), "binary"),
|
|
65
|
+
MAIN_IV: Buffer.from(requireConfigValue(parsed, 'AE_MAIN_IV'), "binary")
|
|
66
|
+
},
|
|
67
|
+
HEADERS: {
|
|
68
|
+
COMMON: {
|
|
69
|
+
'User-Agent': requireConfigValue(parsed, 'HEADERS_COMMON_USER_AGENT'),
|
|
70
|
+
'Connection': requireConfigValue(parsed, 'HEADERS_COMMON_CONNECTION'),
|
|
71
|
+
'Accept-Encoding': requireConfigValue(parsed, 'HEADERS_COMMON_ACCEPT_ENCODING'),
|
|
72
|
+
'Expect': requireConfigValue(parsed, 'HEADERS_COMMON_EXPECT'),
|
|
73
|
+
'X-Unity-Version': requireConfigValue(parsed, 'HEADERS_COMMON_X_UNITY_VERSION'),
|
|
74
|
+
'X-GA': requireConfigValue(parsed, 'HEADERS_COMMON_X_GA'),
|
|
75
|
+
'ReleaseVersion': requireConfigValue(parsed, 'HEADERS_COMMON_RELEASE_VERSION'),
|
|
76
|
+
'Content-Type': requireConfigValue(parsed, 'HEADERS_COMMON_CONTENT_TYPE')
|
|
77
|
+
},
|
|
78
|
+
GARENA_AUTH: {
|
|
79
|
+
'User-Agent': requireConfigValue(parsed, 'HEADERS_GARENA_AUTH_USER_AGENT'),
|
|
80
|
+
'Connection': requireConfigValue(parsed, 'HEADERS_GARENA_AUTH_CONNECTION'),
|
|
81
|
+
'Accept-Encoding': requireConfigValue(parsed, 'HEADERS_GARENA_AUTH_ACCEPT_ENCODING')
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
URLS: {
|
|
85
|
+
GARENA_TOKEN: requireConfigValue(parsed, 'URL_GARENA_TOKEN'),
|
|
86
|
+
MAJOR_LOGIN: requireConfigValue(parsed, 'URL_MAJOR_LOGIN'),
|
|
87
|
+
SEARCH_PATH: requireConfigValue(parsed, 'URL_PATH_SEARCH'),
|
|
88
|
+
PERSONAL_SHOW_PATH: requireConfigValue(parsed, 'URL_PATH_PERSONAL_SHOW'),
|
|
89
|
+
PLAYER_STATS_PATH: requireConfigValue(parsed, 'URL_PATH_PLAYER_STATS'),
|
|
90
|
+
PLAYER_CS_STATS_PATH: requireConfigValue(parsed, 'URL_PATH_PLAYER_CS_STATS')
|
|
91
|
+
},
|
|
92
|
+
GARENA_CLIENT: {
|
|
93
|
+
CLIENT_ID: requireConfigValue(parsed, 'GARENA_CLIENT_ID'),
|
|
94
|
+
CLIENT_SECRET: requireConfigValue(parsed, 'GARENA_CLIENT_SECRET')
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const settings = loadSettings();
|
|
100
|
+
const paths = settings.URLS;
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
AE: settings.AE,
|
|
104
|
+
HEADERS: settings.HEADERS,
|
|
105
|
+
URLS: {
|
|
106
|
+
GARENA_TOKEN: paths.GARENA_TOKEN,
|
|
107
|
+
MAJOR_LOGIN: paths.MAJOR_LOGIN,
|
|
108
|
+
SEARCH: (serverUrl) => `${serverUrl}${paths.SEARCH_PATH}`,
|
|
109
|
+
PERSONAL_SHOW: (serverUrl) => `${serverUrl}${paths.PERSONAL_SHOW_PATH}`,
|
|
110
|
+
PLAYER_STATS: (serverUrl) => `${serverUrl}${paths.PLAYER_STATS_PATH}`,
|
|
111
|
+
PLAYER_CS_STATS: (serverUrl) => `${serverUrl}${paths.PLAYER_CS_STATS_PATH}`
|
|
112
|
+
},
|
|
113
|
+
GARENA_CLIENT: settings.GARENA_CLIENT,
|
|
114
|
+
DEFAULT_CREDENTIALS: loadDefaultCredentials()
|
|
115
|
+
};
|
package/lib/crypto.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const { AE } = require('./constants');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Encrypts data using AES-128-CBC with PKCS7 padding.
|
|
6
|
+
* @param {Buffer} buffer - The data to encrypt
|
|
7
|
+
* @returns {Buffer} Encrypted data
|
|
8
|
+
*/
|
|
9
|
+
function encrypt(buffer) {
|
|
10
|
+
const cipher = crypto.createCipheriv('aes-128-cbc', AE.MAIN_KEY, AE.MAIN_IV);
|
|
11
|
+
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
12
|
+
return encrypted;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { encrypt };
|
package/lib/protobuf.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const protobuf = require('protobufjs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { encrypt } = require('./crypto');
|
|
4
|
+
|
|
5
|
+
const PROTO_DIR = path.join(__dirname, '../proto');
|
|
6
|
+
|
|
7
|
+
class ProtoHandler {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.roots = {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async load(filename) {
|
|
13
|
+
if (!this.roots[filename]) {
|
|
14
|
+
this.roots[filename] = await protobuf.load(path.join(PROTO_DIR, filename));
|
|
15
|
+
}
|
|
16
|
+
return this.roots[filename];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async encode(filename, messageName, payload, shouldEncrypt = true) {
|
|
20
|
+
const root = await this.load(filename);
|
|
21
|
+
const Type = root.lookupType(messageName);
|
|
22
|
+
|
|
23
|
+
// Verify payload matches connection
|
|
24
|
+
const errMsg = Type.verify(payload);
|
|
25
|
+
if (errMsg) throw Error(errMsg);
|
|
26
|
+
|
|
27
|
+
const message = Type.create(payload);
|
|
28
|
+
const buffer = Type.encode(message).finish();
|
|
29
|
+
|
|
30
|
+
if (shouldEncrypt) {
|
|
31
|
+
return encrypt(buffer);
|
|
32
|
+
}
|
|
33
|
+
return buffer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async decode(filename, messageName, buffer) {
|
|
37
|
+
const root = await this.load(filename);
|
|
38
|
+
const Type = root.lookupType(messageName);
|
|
39
|
+
|
|
40
|
+
const message = Type.decode(buffer);
|
|
41
|
+
return Type.toObject(message, {
|
|
42
|
+
longs: String,
|
|
43
|
+
enums: String,
|
|
44
|
+
bytes: String,
|
|
45
|
+
defaults: true,
|
|
46
|
+
arrays: true
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = new ProtoHandler();
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
let itemsDb = null;
|
|
5
|
+
|
|
6
|
+
function loadItems() {
|
|
7
|
+
if (itemsDb) return itemsDb;
|
|
8
|
+
try {
|
|
9
|
+
const data = fs.readFileSync(path.join(__dirname, '../data/items.json'), 'utf8');
|
|
10
|
+
const itemsList = JSON.parse(data);
|
|
11
|
+
itemsDb = {};
|
|
12
|
+
itemsList.forEach(item => {
|
|
13
|
+
const id = item.id || item.itemID;
|
|
14
|
+
if (id) itemsDb[String(id)] = item;
|
|
15
|
+
});
|
|
16
|
+
console.log(`Loaded ${Object.keys(itemsDb).length} items into database.`);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error("Failed to load items database:", e);
|
|
19
|
+
itemsDb = {};
|
|
20
|
+
}
|
|
21
|
+
return itemsDb;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Initialize immediately
|
|
25
|
+
loadItems();
|
|
26
|
+
|
|
27
|
+
function getItemDetails(itemId) {
|
|
28
|
+
const itemStrId = String(itemId);
|
|
29
|
+
|
|
30
|
+
const currentItem = {
|
|
31
|
+
id: itemId,
|
|
32
|
+
name: "Unknown Item",
|
|
33
|
+
type: "UNKNOWN",
|
|
34
|
+
rarity: "NONE",
|
|
35
|
+
description: "",
|
|
36
|
+
is_unique: false,
|
|
37
|
+
image: `https://raw.githubusercontent.com/ashqking/FF-Items/main/ICONS/${itemId}.png`,
|
|
38
|
+
image_fallback: `https://raw.githubusercontent.com/I-SHOW-AKIRU200/AKIRU-ICONS/main/ICONS/${itemId}.png`
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (itemsDb && itemsDb[itemStrId]) {
|
|
42
|
+
const itemData = itemsDb[itemStrId];
|
|
43
|
+
currentItem.name = itemData.name || itemData.description || "Unknown Name";
|
|
44
|
+
currentItem.type = itemData.type || "UNKNOWN";
|
|
45
|
+
currentItem.collection_type = itemData.collection_type || "NONE";
|
|
46
|
+
currentItem.rarity = itemData.rare || "NONE";
|
|
47
|
+
currentItem.description = itemData.description || "";
|
|
48
|
+
currentItem.is_unique = itemData.is_unique || false;
|
|
49
|
+
currentItem.icon_code = itemData.icon || "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return currentItem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function processPlayerItems(playerData) {
|
|
56
|
+
const profileInfo = playerData.profileinfo || {};
|
|
57
|
+
const basicInfo = playerData.basicinfo || {};
|
|
58
|
+
const petInfo = playerData.petinfo || {};
|
|
59
|
+
|
|
60
|
+
// Clothes
|
|
61
|
+
const outfitIds = profileInfo.clothes || [];
|
|
62
|
+
const outfitDetails = outfitIds.map(getItemDetails);
|
|
63
|
+
|
|
64
|
+
// Weapons (shown skins)
|
|
65
|
+
const weaponIds = basicInfo.weaponskinshows || [];
|
|
66
|
+
const weaponDetails = weaponIds.map(getItemDetails);
|
|
67
|
+
|
|
68
|
+
// Skills
|
|
69
|
+
const skillIds = profileInfo.equipedskills || [];
|
|
70
|
+
const skillDetails = skillIds.map(getItemDetails);
|
|
71
|
+
|
|
72
|
+
// Pet
|
|
73
|
+
let petDetails = null;
|
|
74
|
+
if (petInfo && (petInfo.id || petInfo.skinid)) {
|
|
75
|
+
petDetails = {
|
|
76
|
+
id: getItemDetails(petInfo.id),
|
|
77
|
+
name: petInfo.name,
|
|
78
|
+
level: petInfo.level,
|
|
79
|
+
skin: getItemDetails(petInfo.skinid),
|
|
80
|
+
selected_skill: getItemDetails(petInfo.selectedskillid)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const normalizedBasicInfo = {
|
|
85
|
+
accountid: basicInfo.accountid || "",
|
|
86
|
+
nickname: basicInfo.nickname || "",
|
|
87
|
+
level: basicInfo.level || 0,
|
|
88
|
+
region: basicInfo.region || "",
|
|
89
|
+
liked: basicInfo.liked || "",
|
|
90
|
+
signature: basicInfo.signature || ""
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
basic_info: normalizedBasicInfo,
|
|
95
|
+
items: {
|
|
96
|
+
outfit: outfitDetails,
|
|
97
|
+
skills: {
|
|
98
|
+
equipped: skillDetails
|
|
99
|
+
},
|
|
100
|
+
weapons: {
|
|
101
|
+
shown_skins: weaponDetails
|
|
102
|
+
},
|
|
103
|
+
pet: petDetails
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
loadItems,
|
|
110
|
+
getItemDetails,
|
|
111
|
+
processPlayerItems
|
|
112
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spinzaf/freefire-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A powerful Node.js library to interact with Garena Free Fire API using Protobuf. Login, Search Players, and get Profile Stats.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node test/search.js",
|
|
8
|
+
"test:login": "node test/login.js",
|
|
9
|
+
"test:search": "node test/search.js",
|
|
10
|
+
"test:profile": "node test/profile.js",
|
|
11
|
+
"test:stats": "node test/stats.js",
|
|
12
|
+
"test:items": "node test/items.js",
|
|
13
|
+
"test:all": "node test/all.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"freefire",
|
|
17
|
+
"checker",
|
|
18
|
+
"game-api",
|
|
19
|
+
"player-stats",
|
|
20
|
+
"ff",
|
|
21
|
+
"freefire-api",
|
|
22
|
+
"garena-freefire",
|
|
23
|
+
"player-info",
|
|
24
|
+
"stats-checker",
|
|
25
|
+
"game-data",
|
|
26
|
+
"sdk",
|
|
27
|
+
"wrapper",
|
|
28
|
+
"nodejs",
|
|
29
|
+
"javascript"
|
|
30
|
+
],
|
|
31
|
+
"author": "spinzaf",
|
|
32
|
+
"license": "GPL-3.0",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/spinzaf/freefire-api.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/spinzaf/freefire-api/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/spinzaf/freefire-api#readme",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"axios": "^1.6.0",
|
|
43
|
+
"protobufjs": "^7.2.0"
|
|
44
|
+
}
|
|
45
|
+
}
|