ffapis 1.5.2
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/README.md +46 -0
- package/config/credentials/IND.yaml +219 -0
- package/config/credentials/RU.yaml +84 -0
- package/config/credentials/SG.yaml +220 -0
- package/config/settings.yaml +28 -0
- package/data/items.json +153075 -0
- package/dist/index.d.mts +352 -0
- package/dist/index.d.ts +352 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1300 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1120 -0
- package/dist/types/index.d.ts +147 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +11 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +82 -0
- package/proto/MajorLogin.proto +173 -0
- package/proto/MajorRegister.proto +22 -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/dist/index.mjs
ADDED
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GARENA_CLIENT,
|
|
3
|
+
HEADERS,
|
|
4
|
+
URLS,
|
|
5
|
+
__toCommonJS,
|
|
6
|
+
crypto_exports,
|
|
7
|
+
encrypt,
|
|
8
|
+
init_constants,
|
|
9
|
+
init_crypto,
|
|
10
|
+
init_resolve_path,
|
|
11
|
+
resolveProjectDir,
|
|
12
|
+
resolveProjectFile
|
|
13
|
+
} from "./chunk-GUUYOQJ2.mjs";
|
|
14
|
+
|
|
15
|
+
// src/lib/api.ts
|
|
16
|
+
import axios from "axios";
|
|
17
|
+
import crypto from "crypto";
|
|
18
|
+
|
|
19
|
+
// src/lib/protobuf.ts
|
|
20
|
+
init_crypto();
|
|
21
|
+
init_resolve_path();
|
|
22
|
+
import protobuf from "protobufjs";
|
|
23
|
+
import path from "path";
|
|
24
|
+
var PROTO_DIR = resolveProjectDir("proto");
|
|
25
|
+
var ProtoHandler = class {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.roots = {};
|
|
28
|
+
}
|
|
29
|
+
async load(filename) {
|
|
30
|
+
if (!this.roots[filename]) {
|
|
31
|
+
this.roots[filename] = await protobuf.load(path.join(PROTO_DIR, filename));
|
|
32
|
+
}
|
|
33
|
+
return this.roots[filename];
|
|
34
|
+
}
|
|
35
|
+
async encode(filename, messageName, payload, shouldEncrypt = true) {
|
|
36
|
+
const root = await this.load(filename);
|
|
37
|
+
const Type = root.lookupType(messageName);
|
|
38
|
+
const errMsg = Type.verify(payload);
|
|
39
|
+
if (errMsg) throw new Error(errMsg);
|
|
40
|
+
const message = Type.create(payload);
|
|
41
|
+
const buffer = Type.encode(message).finish();
|
|
42
|
+
if (shouldEncrypt) {
|
|
43
|
+
return encrypt(Buffer.from(buffer));
|
|
44
|
+
}
|
|
45
|
+
return Buffer.from(buffer);
|
|
46
|
+
}
|
|
47
|
+
async decode(filename, messageName, buffer) {
|
|
48
|
+
const root = await this.load(filename);
|
|
49
|
+
const Type = root.lookupType(messageName);
|
|
50
|
+
const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
|
51
|
+
const message = Type.decode(buf);
|
|
52
|
+
return Type.toObject(message, {
|
|
53
|
+
longs: String,
|
|
54
|
+
enums: String,
|
|
55
|
+
bytes: String,
|
|
56
|
+
defaults: true,
|
|
57
|
+
arrays: true
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var protoHandler = new ProtoHandler();
|
|
62
|
+
|
|
63
|
+
// src/lib/api.ts
|
|
64
|
+
init_constants();
|
|
65
|
+
|
|
66
|
+
// src/lib/utils.ts
|
|
67
|
+
import fs from "fs";
|
|
68
|
+
|
|
69
|
+
// src/types/index.ts
|
|
70
|
+
function getErrorMessage(error) {
|
|
71
|
+
if (error instanceof Error) return error.message;
|
|
72
|
+
if (typeof error === "string") return error;
|
|
73
|
+
return String(error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/lib/utils.ts
|
|
77
|
+
init_resolve_path();
|
|
78
|
+
function coerceString(value, fallback = "") {
|
|
79
|
+
if (typeof value === "string") return value;
|
|
80
|
+
if (value === null || value === void 0) return fallback;
|
|
81
|
+
return String(value);
|
|
82
|
+
}
|
|
83
|
+
function coerceBoolean(value, fallback = false) {
|
|
84
|
+
if (typeof value === "boolean") return value;
|
|
85
|
+
return fallback;
|
|
86
|
+
}
|
|
87
|
+
var itemsDb = null;
|
|
88
|
+
function loadItems() {
|
|
89
|
+
if (itemsDb) return itemsDb;
|
|
90
|
+
try {
|
|
91
|
+
const data = fs.readFileSync(resolveProjectFile("data/items.json"), "utf8");
|
|
92
|
+
const itemsList = JSON.parse(data);
|
|
93
|
+
itemsDb = {};
|
|
94
|
+
for (const item of itemsList) {
|
|
95
|
+
const rawId = item.id ?? item.itemID;
|
|
96
|
+
const id = typeof rawId === "string" || typeof rawId === "number" ? rawId : void 0;
|
|
97
|
+
if (id !== void 0) itemsDb[String(id)] = item;
|
|
98
|
+
}
|
|
99
|
+
console.log(`Loaded ${Object.keys(itemsDb).length} items into database.`);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error("Failed to load items database:", getErrorMessage(e));
|
|
102
|
+
itemsDb = {};
|
|
103
|
+
}
|
|
104
|
+
return itemsDb;
|
|
105
|
+
}
|
|
106
|
+
function getItemDetails(itemId) {
|
|
107
|
+
const db = loadItems();
|
|
108
|
+
const itemStrId = String(itemId);
|
|
109
|
+
const currentItem = {
|
|
110
|
+
id: itemId,
|
|
111
|
+
name: "Unknown Item",
|
|
112
|
+
type: "UNKNOWN",
|
|
113
|
+
rarity: "NONE",
|
|
114
|
+
description: "",
|
|
115
|
+
is_unique: false,
|
|
116
|
+
image: `https://raw.githubusercontent.com/ashqking/FF-Items/main/ICONS/${itemId}.png`,
|
|
117
|
+
image_fallback: `https://raw.githubusercontent.com/I-SHOW-AKIRU200/AKIRU-ICONS/main/ICONS/${itemId}.png`
|
|
118
|
+
};
|
|
119
|
+
if (db[itemStrId]) {
|
|
120
|
+
const itemData = db[itemStrId];
|
|
121
|
+
currentItem.name = coerceString(itemData.name ?? itemData.description ?? "Unknown Name");
|
|
122
|
+
currentItem.type = coerceString(itemData.type ?? "UNKNOWN");
|
|
123
|
+
currentItem.collection_type = coerceString(itemData.collection_type ?? "NONE");
|
|
124
|
+
currentItem.rarity = coerceString(itemData.rare ?? "NONE");
|
|
125
|
+
currentItem.description = coerceString(itemData.description ?? "");
|
|
126
|
+
currentItem.is_unique = coerceBoolean(itemData.is_unique ?? false);
|
|
127
|
+
currentItem.icon_code = coerceString(itemData.icon ?? "");
|
|
128
|
+
}
|
|
129
|
+
return currentItem;
|
|
130
|
+
}
|
|
131
|
+
function processPlayerItems(playerData) {
|
|
132
|
+
const profileInfo = playerData.profileinfo;
|
|
133
|
+
const basicInfo = playerData.basicinfo;
|
|
134
|
+
const petInfo = playerData.petinfo;
|
|
135
|
+
const outfitIds = profileInfo.clothes ?? [];
|
|
136
|
+
const outfitDetails = outfitIds.map(getItemDetails);
|
|
137
|
+
const weaponIds = basicInfo.weaponskinshows ?? [];
|
|
138
|
+
const weaponDetails = weaponIds.map(getItemDetails);
|
|
139
|
+
const skillIds = profileInfo.equipedskills ?? [];
|
|
140
|
+
const skillDetails = skillIds.map(getItemDetails);
|
|
141
|
+
let petDetails = null;
|
|
142
|
+
if (petInfo && (petInfo.id || petInfo.skinid)) {
|
|
143
|
+
petDetails = {
|
|
144
|
+
id: getItemDetails(petInfo.id),
|
|
145
|
+
name: petInfo.name || "",
|
|
146
|
+
level: petInfo.level || 0,
|
|
147
|
+
skin: getItemDetails(petInfo.skinid),
|
|
148
|
+
selected_skill: getItemDetails(petInfo.selectedskillid)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const normalizedBasicInfo = {
|
|
152
|
+
accountid: basicInfo.accountid || "",
|
|
153
|
+
nickname: basicInfo.nickname || "",
|
|
154
|
+
level: basicInfo.level || 0,
|
|
155
|
+
region: basicInfo.region || "",
|
|
156
|
+
liked: basicInfo.liked || "",
|
|
157
|
+
signature: basicInfo.signature || ""
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
basic_info: normalizedBasicInfo,
|
|
161
|
+
items: {
|
|
162
|
+
outfit: outfitDetails,
|
|
163
|
+
skills: { equipped: skillDetails },
|
|
164
|
+
weapons: { shown_skins: weaponDetails },
|
|
165
|
+
pet: petDetails
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/lib/credential-manager.ts
|
|
171
|
+
import fs2 from "fs";
|
|
172
|
+
init_resolve_path();
|
|
173
|
+
var CredentialManager = class {
|
|
174
|
+
/**
|
|
175
|
+
* @param region - Region code whose credential YAML file will be loaded.
|
|
176
|
+
*/
|
|
177
|
+
constructor(region) {
|
|
178
|
+
this.pool = [];
|
|
179
|
+
this.currentIndex = 0;
|
|
180
|
+
this.usageData = {};
|
|
181
|
+
this.region = region;
|
|
182
|
+
this._loadPool();
|
|
183
|
+
}
|
|
184
|
+
_loadPool() {
|
|
185
|
+
const filePath = resolveProjectFile(`config/credentials/${this.region}.yaml`);
|
|
186
|
+
try {
|
|
187
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
188
|
+
const lines = content.split("\n");
|
|
189
|
+
let currentAccount = null;
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
const trimmed = line.trim();
|
|
192
|
+
if (trimmed.startsWith("- uid:")) {
|
|
193
|
+
if (currentAccount) {
|
|
194
|
+
this.pool.push(currentAccount);
|
|
195
|
+
}
|
|
196
|
+
const uidMatch = trimmed.match(/uid:\s*"([^"]+)"/);
|
|
197
|
+
currentAccount = { uid: uidMatch ? uidMatch[1] : "", password: "" };
|
|
198
|
+
} else if (trimmed.startsWith("password:") && currentAccount) {
|
|
199
|
+
const pwdMatch = trimmed.match(/password:\s*"([^"]+)"/);
|
|
200
|
+
if (pwdMatch) {
|
|
201
|
+
currentAccount.password = pwdMatch[1];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (currentAccount && currentAccount.password) {
|
|
206
|
+
this.pool.push(currentAccount);
|
|
207
|
+
}
|
|
208
|
+
console.log(`[CredentialManager] Loaded ${this.pool.length} accounts for ${this.region}`);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(`[CredentialManager] Failed to load credentials for ${this.region}:`, getErrorMessage(error));
|
|
211
|
+
this.pool = [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
isUsedForTarget(targetUid, guestUid) {
|
|
215
|
+
if (!this.usageData[targetUid]) return false;
|
|
216
|
+
return this.usageData[targetUid].used_guests[guestUid] !== void 0;
|
|
217
|
+
}
|
|
218
|
+
markUsed(targetUid, guestUid) {
|
|
219
|
+
if (!this.usageData[targetUid]) {
|
|
220
|
+
this.usageData[targetUid] = { used_guests: {}, total_likes: 0 };
|
|
221
|
+
}
|
|
222
|
+
this.usageData[targetUid].used_guests[guestUid] = (/* @__PURE__ */ new Date()).toISOString();
|
|
223
|
+
this.usageData[targetUid].total_likes = Object.keys(this.usageData[targetUid].used_guests).length;
|
|
224
|
+
}
|
|
225
|
+
getRandomCredential() {
|
|
226
|
+
if (this.pool.length === 0) return null;
|
|
227
|
+
const randomIndex = Math.floor(Math.random() * this.pool.length);
|
|
228
|
+
return this.pool[randomIndex];
|
|
229
|
+
}
|
|
230
|
+
getNextCredential() {
|
|
231
|
+
if (this.pool.length === 0) return null;
|
|
232
|
+
const cred = this.pool[this.currentIndex];
|
|
233
|
+
this.currentIndex = (this.currentIndex + 1) % this.pool.length;
|
|
234
|
+
return cred;
|
|
235
|
+
}
|
|
236
|
+
getNextForTarget(targetUid) {
|
|
237
|
+
const available = this.pool.filter((acc) => !this.isUsedForTarget(targetUid, acc.uid));
|
|
238
|
+
if (available.length === 0) return null;
|
|
239
|
+
return available[0];
|
|
240
|
+
}
|
|
241
|
+
getMultipleForTarget(targetUid, count) {
|
|
242
|
+
const available = this.pool.filter((acc) => !this.isUsedForTarget(targetUid, acc.uid));
|
|
243
|
+
return available.slice(0, count);
|
|
244
|
+
}
|
|
245
|
+
getAvailableCount(targetUid) {
|
|
246
|
+
return this.pool.filter((acc) => !this.isUsedForTarget(targetUid, acc.uid)).length;
|
|
247
|
+
}
|
|
248
|
+
getPoolSize() {
|
|
249
|
+
return this.pool.length;
|
|
250
|
+
}
|
|
251
|
+
clearUsage(targetUid) {
|
|
252
|
+
if (targetUid) {
|
|
253
|
+
delete this.usageData[targetUid];
|
|
254
|
+
} else {
|
|
255
|
+
this.usageData = {};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/lib/api.ts
|
|
261
|
+
import fs3 from "fs";
|
|
262
|
+
import path2 from "path";
|
|
263
|
+
init_resolve_path();
|
|
264
|
+
var FreeFireAPI = class {
|
|
265
|
+
/**
|
|
266
|
+
* Creates a new FreeFireAPI instance.
|
|
267
|
+
* @param region - Optional region code (e.g., 'IND', 'BR') to scope credential lookup.
|
|
268
|
+
*/
|
|
269
|
+
constructor(region = null) {
|
|
270
|
+
this.allCredentials = null;
|
|
271
|
+
this.session = { token: null, serverUrl: null, openId: null, accountId: null };
|
|
272
|
+
this.region = region;
|
|
273
|
+
this.credentialManager = region ? new CredentialManager(region) : null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Switches the active region and initializes a new credential manager.
|
|
277
|
+
* @param region - Region code to switch to.
|
|
278
|
+
*/
|
|
279
|
+
setRegion(region) {
|
|
280
|
+
this.region = region;
|
|
281
|
+
this.credentialManager = new CredentialManager(region);
|
|
282
|
+
}
|
|
283
|
+
_loadAllCredentials() {
|
|
284
|
+
if (this.allCredentials) return this.allCredentials;
|
|
285
|
+
const allCreds = [];
|
|
286
|
+
const credentialsDir = resolveProjectDir("config/credentials");
|
|
287
|
+
try {
|
|
288
|
+
const files = fs3.readdirSync(credentialsDir);
|
|
289
|
+
for (const file of files) {
|
|
290
|
+
if (file.endsWith(".yaml")) {
|
|
291
|
+
const filePath = path2.join(credentialsDir, file);
|
|
292
|
+
const content = fs3.readFileSync(filePath, "utf8");
|
|
293
|
+
const lines = content.split("\n");
|
|
294
|
+
let currentAccount = null;
|
|
295
|
+
for (const line of lines) {
|
|
296
|
+
const trimmed = line.trim();
|
|
297
|
+
if (trimmed.startsWith("- uid:")) {
|
|
298
|
+
if (currentAccount) allCreds.push(currentAccount);
|
|
299
|
+
const uidMatch = trimmed.match(/uid:\s*"([^"]+)"/);
|
|
300
|
+
currentAccount = { uid: uidMatch ? uidMatch[1] : "", password: "" };
|
|
301
|
+
} else if (trimmed.startsWith("password:") && currentAccount) {
|
|
302
|
+
const pwdMatch = trimmed.match(/password:\s*"([^"]+)"/);
|
|
303
|
+
if (pwdMatch) currentAccount.password = pwdMatch[1];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (currentAccount && currentAccount.password) allCreds.push(currentAccount);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error("[API] Failed to load all credentials:", getErrorMessage(error));
|
|
311
|
+
}
|
|
312
|
+
this.allCredentials = allCreds;
|
|
313
|
+
console.log(`[API] Loaded ${allCreds.length} credentials from all regions`);
|
|
314
|
+
return allCreds;
|
|
315
|
+
}
|
|
316
|
+
_getRandomCredentialFromAll() {
|
|
317
|
+
const creds = this._loadAllCredentials();
|
|
318
|
+
if (creds.length === 0) throw new Error("No credentials available in any region");
|
|
319
|
+
const randomIndex = Math.floor(Math.random() * creds.length);
|
|
320
|
+
return creds[randomIndex];
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Logs in using a random credential from any available region pool.
|
|
324
|
+
* @returns A valid session containing token, server URL, and account details.
|
|
325
|
+
*/
|
|
326
|
+
async loginWithRandomCredentialFromAll() {
|
|
327
|
+
const cred = this._getRandomCredentialFromAll();
|
|
328
|
+
console.log(`[API] Using random credential from all regions: ${cred.uid}`);
|
|
329
|
+
return this.login(cred.uid, cred.password);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Logs in using a random credential from the currently set region pool.
|
|
333
|
+
* Falls back to all-region lookup if no region is configured.
|
|
334
|
+
* @returns A valid session containing token, server URL, and account details.
|
|
335
|
+
*/
|
|
336
|
+
async loginWithRandomCredential() {
|
|
337
|
+
if (!this.credentialManager) {
|
|
338
|
+
return this.loginWithRandomCredentialFromAll();
|
|
339
|
+
}
|
|
340
|
+
const cred = this.credentialManager.getRandomCredential();
|
|
341
|
+
if (!cred) throw new Error(`No credentials available in pool for region ${this.region}`);
|
|
342
|
+
console.log(`[API] Using random credential from ${this.region}: ${cred.uid}`);
|
|
343
|
+
return this.login(cred.uid, cred.password);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Authenticates with a specific UID and password.
|
|
347
|
+
* @param uid - Garena account UID.
|
|
348
|
+
* @param password - Account password.
|
|
349
|
+
* @returns A valid session containing token, server URL, and account details.
|
|
350
|
+
*/
|
|
351
|
+
async login(uid, password) {
|
|
352
|
+
if (!uid || !password) throw new Error("Missing credentials. Please provide UID and PASSWORD to login(uid, password).");
|
|
353
|
+
const garenaData = await this._getGarenaToken(uid, password);
|
|
354
|
+
if (!garenaData?.access_token) throw new Error("Garena authentication failed: Invalid credentials or response");
|
|
355
|
+
const loginData = await this._majorLogin(garenaData.access_token, garenaData.open_id);
|
|
356
|
+
if (!loginData?.token) throw new Error("Major login failed: Empty token received");
|
|
357
|
+
this.session.token = loginData.token;
|
|
358
|
+
this.session.serverUrl = loginData.serverUrl;
|
|
359
|
+
this.session.openId = garenaData.open_id;
|
|
360
|
+
this.session.accountId = loginData.accountid;
|
|
361
|
+
return this.session;
|
|
362
|
+
}
|
|
363
|
+
async _getGarenaToken(uid, password) {
|
|
364
|
+
const params = new URLSearchParams();
|
|
365
|
+
params.append("uid", uid);
|
|
366
|
+
params.append("password", password);
|
|
367
|
+
params.append("response_type", "token");
|
|
368
|
+
params.append("client_type", "2");
|
|
369
|
+
params.append("client_secret", GARENA_CLIENT.CLIENT_SECRET);
|
|
370
|
+
params.append("client_id", GARENA_CLIENT.CLIENT_ID);
|
|
371
|
+
try {
|
|
372
|
+
const response = await axios.post(URLS.GARENA_TOKEN, params, { headers: HEADERS.GARENA_AUTH, timeout: 3e4 });
|
|
373
|
+
return response.data;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(`Garena Auth Request Failed: ${getErrorMessage(error)}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async _majorLogin(accessToken, openId) {
|
|
379
|
+
const payload = { openid: openId, logintoken: accessToken, platform: "4" };
|
|
380
|
+
const encryptedBody = await protoHandler.encode("MajorLogin.proto", "request", payload, true);
|
|
381
|
+
try {
|
|
382
|
+
const response = await axios.post(URLS.MAJOR_LOGIN, encryptedBody, {
|
|
383
|
+
headers: {
|
|
384
|
+
...HEADERS.COMMON,
|
|
385
|
+
Authorization: "Bearer",
|
|
386
|
+
"Content-Type": "application/octet-stream"
|
|
387
|
+
},
|
|
388
|
+
responseType: "arraybuffer",
|
|
389
|
+
timeout: 3e4
|
|
390
|
+
});
|
|
391
|
+
const decoded = await protoHandler.decode("MajorLogin.proto", "response", response.data);
|
|
392
|
+
return decoded;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw new Error(`Major Login Request Failed: ${getErrorMessage(error)}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Searches for players by nickname across Free Fire servers.
|
|
399
|
+
* @param keyword - Player nickname to search (minimum 3 characters).
|
|
400
|
+
* @returns Array of matching player results.
|
|
401
|
+
*/
|
|
402
|
+
async searchAccount(keyword) {
|
|
403
|
+
if (!this.session.token) await this.loginWithRandomCredential();
|
|
404
|
+
if (keyword.length < 3) throw new Error("Search keyword must be at least 3 characters long.");
|
|
405
|
+
const payload = { keyword: String(keyword) };
|
|
406
|
+
const encryptedBody = await protoHandler.encode("SearchAccountByName.proto", "SearchAccountByName.request", payload, true);
|
|
407
|
+
const url = URLS.SEARCH(this.session.serverUrl);
|
|
408
|
+
try {
|
|
409
|
+
const response = await axios.post(url, encryptedBody, {
|
|
410
|
+
headers: {
|
|
411
|
+
...HEADERS.COMMON,
|
|
412
|
+
Authorization: `Bearer ${this.session.token}`,
|
|
413
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
414
|
+
},
|
|
415
|
+
responseType: "arraybuffer",
|
|
416
|
+
timeout: 3e4
|
|
417
|
+
});
|
|
418
|
+
const data = await protoHandler.decode("SearchAccountByName.proto", "SearchAccountByName.response", response.data);
|
|
419
|
+
return data.infos || [];
|
|
420
|
+
} catch (error) {
|
|
421
|
+
throw new Error(`Search Failed: ${getErrorMessage(error)}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Retrieves detailed profile information for a player.
|
|
426
|
+
* @param uid - Target player UID.
|
|
427
|
+
* @returns Structured player profile including basic info, clan, and pet data.
|
|
428
|
+
*/
|
|
429
|
+
async getPlayerProfile(uid) {
|
|
430
|
+
return this._requestProfile(uid, false);
|
|
431
|
+
}
|
|
432
|
+
async _requestProfile(uid, isRetry) {
|
|
433
|
+
await this._checkSession();
|
|
434
|
+
const payload = { accountId: Number(uid), callSignSrc: 7, needGalleryInfo: true };
|
|
435
|
+
const encryptedBody = await protoHandler.encode("PlayerPersonalShow.proto", "request", payload, true);
|
|
436
|
+
const url = URLS.PERSONAL_SHOW(this.session.serverUrl);
|
|
437
|
+
try {
|
|
438
|
+
const response = await axios.post(url, encryptedBody, {
|
|
439
|
+
headers: { ...HEADERS.COMMON, Authorization: `Bearer ${this.session.token}` },
|
|
440
|
+
responseType: "arraybuffer",
|
|
441
|
+
timeout: 3e4
|
|
442
|
+
});
|
|
443
|
+
const decoded = await protoHandler.decode("PlayerPersonalShow.proto", "response", response.data);
|
|
444
|
+
return decoded;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
const status = axios.isAxiosError(error) ? error.response?.status : 0;
|
|
447
|
+
if (!isRetry && (status === 400 || status === 401)) {
|
|
448
|
+
this.session.token = null;
|
|
449
|
+
await this._checkSession();
|
|
450
|
+
return this._requestProfile(uid, true);
|
|
451
|
+
}
|
|
452
|
+
throw new Error(`Get Profile Failed: ${getErrorMessage(error)}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Fetches and processes a player's equipped items (outfit, weapons, skills, pet).
|
|
457
|
+
* @param uid - Target player UID.
|
|
458
|
+
* @returns Normalized item details mapped from the internal items database.
|
|
459
|
+
*/
|
|
460
|
+
async getPlayerItems(uid) {
|
|
461
|
+
const profile = await this.getPlayerProfile(uid);
|
|
462
|
+
if (!profile) return null;
|
|
463
|
+
return processPlayerItems(profile);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Retrieves match statistics for a player.
|
|
467
|
+
* @param uid - Target player UID.
|
|
468
|
+
* @param mode - Game mode: 'br' (Battle Royale) or 'cs' (Clash Squad).
|
|
469
|
+
* @param matchType - Match type: 'career', 'ranked', or 'normal'.
|
|
470
|
+
* @returns Structured stats object for solo, duo, and squad matches.
|
|
471
|
+
*/
|
|
472
|
+
async getPlayerStats(uid, mode = "br", matchType = "career") {
|
|
473
|
+
if (!this.session.token) await this.loginWithRandomCredential();
|
|
474
|
+
const modeLower = mode.toLowerCase();
|
|
475
|
+
const typeUpper = matchType.toUpperCase();
|
|
476
|
+
let matchMode = 0;
|
|
477
|
+
let url = "";
|
|
478
|
+
let protoFile = "";
|
|
479
|
+
const payload = { accountid: Number(uid) };
|
|
480
|
+
if (modeLower === "br") {
|
|
481
|
+
const types = { CAREER: 0, NORMAL: 1, RANKED: 2 };
|
|
482
|
+
matchMode = types[typeUpper] !== void 0 ? types[typeUpper] : 0;
|
|
483
|
+
url = URLS.PLAYER_STATS(this.session.serverUrl);
|
|
484
|
+
protoFile = "PlayerStats.proto";
|
|
485
|
+
payload.matchmode = matchMode;
|
|
486
|
+
} else {
|
|
487
|
+
const types = { CAREER: 0, NORMAL: 1, RANKED: 6 };
|
|
488
|
+
matchMode = types[typeUpper] !== void 0 ? types[typeUpper] : 0;
|
|
489
|
+
url = URLS.PLAYER_CS_STATS(this.session.serverUrl);
|
|
490
|
+
protoFile = "PlayerCSStats.proto";
|
|
491
|
+
payload.gamemode = 15;
|
|
492
|
+
payload.matchmode = matchMode;
|
|
493
|
+
}
|
|
494
|
+
const encryptedBody = await protoHandler.encode(protoFile, "request", payload, true);
|
|
495
|
+
try {
|
|
496
|
+
const response = await axios.post(url, encryptedBody, {
|
|
497
|
+
headers: { ...HEADERS.COMMON, Authorization: `Bearer ${this.session.token}` },
|
|
498
|
+
responseType: "arraybuffer",
|
|
499
|
+
timeout: 3e4
|
|
500
|
+
});
|
|
501
|
+
const decoded = await protoHandler.decode(protoFile, "response", response.data);
|
|
502
|
+
return decoded;
|
|
503
|
+
} catch (error) {
|
|
504
|
+
throw new Error(`Get Stats Failed: ${getErrorMessage(error)}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async _checkSession() {
|
|
508
|
+
if (!this.session.token || !this.session.serverUrl) {
|
|
509
|
+
await this.loginWithRandomCredentialFromAll();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Registers a new guest account in the specified region.
|
|
514
|
+
* @param region - Target region code (e.g., 'IND').
|
|
515
|
+
* @param nickname - Optional nickname; a random one is generated if omitted.
|
|
516
|
+
* @returns Registration result containing UID, password, and region.
|
|
517
|
+
*/
|
|
518
|
+
async register(region, nickname = null) {
|
|
519
|
+
const password = this._generateRandomPassword();
|
|
520
|
+
const passwordHash = crypto.createHash("sha256").update(password).digest("hex").toUpperCase();
|
|
521
|
+
const uid = await this._guestRegister(passwordHash);
|
|
522
|
+
if (!uid) throw new Error("Guest registration failed");
|
|
523
|
+
const garenaData = await this._getGarenaTokenForRegister(uid, passwordHash);
|
|
524
|
+
if (!garenaData?.access_token) throw new Error("Token grant failed after registration");
|
|
525
|
+
const autoNickname = nickname || `senos${Math.floor(Math.random() * 9999) + 1}`;
|
|
526
|
+
const registerData = await this._majorRegister(autoNickname, garenaData.access_token, garenaData.open_id, region);
|
|
527
|
+
if (!registerData.success) throw new Error(`Major registration failed: ${registerData.error || "Unknown error"}`);
|
|
528
|
+
return { uid, password, passwordHash, region, nickname: autoNickname };
|
|
529
|
+
}
|
|
530
|
+
_generateRandomPassword() {
|
|
531
|
+
return String(Math.floor(Math.random() * 9e9) + 1e9);
|
|
532
|
+
}
|
|
533
|
+
async _guestRegister(passwordHash) {
|
|
534
|
+
const params = new URLSearchParams();
|
|
535
|
+
params.append("password", passwordHash);
|
|
536
|
+
params.append("client_type", "2");
|
|
537
|
+
params.append("source", "2");
|
|
538
|
+
params.append("app_id", GARENA_CLIENT.CLIENT_ID);
|
|
539
|
+
const signature = crypto.createHmac("sha256", GARENA_CLIENT.CLIENT_SECRET).update(params.toString()).digest("hex");
|
|
540
|
+
try {
|
|
541
|
+
const response = await axios.post(URLS.GUEST_REGISTER, params, {
|
|
542
|
+
headers: {
|
|
543
|
+
...HEADERS.GARENA_AUTH,
|
|
544
|
+
Authorization: `Signature ${signature}`,
|
|
545
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
546
|
+
},
|
|
547
|
+
timeout: 3e4
|
|
548
|
+
});
|
|
549
|
+
return response.data.uid;
|
|
550
|
+
} catch (error) {
|
|
551
|
+
throw new Error(`Guest Register Failed: ${getErrorMessage(error)}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async _getGarenaTokenForRegister(uid, passwordHash) {
|
|
555
|
+
const params = new URLSearchParams();
|
|
556
|
+
params.append("uid", uid);
|
|
557
|
+
params.append("password", passwordHash);
|
|
558
|
+
params.append("response_type", "token");
|
|
559
|
+
params.append("client_type", "2");
|
|
560
|
+
params.append("client_secret", GARENA_CLIENT.CLIENT_SECRET);
|
|
561
|
+
params.append("client_id", GARENA_CLIENT.CLIENT_ID);
|
|
562
|
+
try {
|
|
563
|
+
const response = await axios.post(URLS.GARENA_TOKEN, params, { headers: HEADERS.GARENA_AUTH, timeout: 3e4 });
|
|
564
|
+
return response.data;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
throw new Error(`Token Grant Failed: ${getErrorMessage(error)}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
_xorEncryptOpenId(openId) {
|
|
570
|
+
const k = [0, 0, 0, 2, 0, 1, 7, 0, 0, 0, 0, 0, 2, 0, 1, 7, 0, 0, 0, 0, 0, 2, 0, 1, 7, 0, 0, 0, 0, 0, 2, 0];
|
|
571
|
+
const bytes = Buffer.from(openId, "utf8");
|
|
572
|
+
const result = Buffer.alloc(bytes.length);
|
|
573
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
574
|
+
result[i] = bytes[i] ^ k[i % k.length] ^ 48;
|
|
575
|
+
}
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
_encodeVarint(n) {
|
|
579
|
+
const result = [];
|
|
580
|
+
while (n > 127) {
|
|
581
|
+
result.push(n & 127 | 128);
|
|
582
|
+
n >>= 7;
|
|
583
|
+
}
|
|
584
|
+
result.push(n);
|
|
585
|
+
return Buffer.from(result);
|
|
586
|
+
}
|
|
587
|
+
_encodeField(fieldNum, value) {
|
|
588
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
589
|
+
const tag = fieldNum << 3 | 0;
|
|
590
|
+
const varint = this._encodeVarint(value);
|
|
591
|
+
return Buffer.concat([this._encodeVarint(tag), varint]);
|
|
592
|
+
}
|
|
593
|
+
if (typeof value === "string") {
|
|
594
|
+
const bytes = Buffer.from(value, "utf8");
|
|
595
|
+
const tag = fieldNum << 3 | 2;
|
|
596
|
+
return Buffer.concat([this._encodeVarint(tag), this._encodeVarint(bytes.length), bytes]);
|
|
597
|
+
}
|
|
598
|
+
if (Buffer.isBuffer(value)) {
|
|
599
|
+
const tag = fieldNum << 3 | 2;
|
|
600
|
+
return Buffer.concat([this._encodeVarint(tag), this._encodeVarint(value.length), value]);
|
|
601
|
+
}
|
|
602
|
+
throw new Error("Unsupported protobuf field type");
|
|
603
|
+
}
|
|
604
|
+
_manualProtobufEncode(data) {
|
|
605
|
+
const parts = [];
|
|
606
|
+
const entries = Object.entries(data).sort((a, b) => Number(a[0]) - Number(b[0]));
|
|
607
|
+
for (const [fieldNum, value] of entries) {
|
|
608
|
+
parts.push(this._encodeField(Number(fieldNum), value));
|
|
609
|
+
}
|
|
610
|
+
return Buffer.concat(parts);
|
|
611
|
+
}
|
|
612
|
+
async _majorRegister(nickname, accessToken, openId, region) {
|
|
613
|
+
const encryptedOpenId = this._xorEncryptOpenId(openId);
|
|
614
|
+
const payload = {
|
|
615
|
+
1: nickname,
|
|
616
|
+
2: accessToken,
|
|
617
|
+
3: openId,
|
|
618
|
+
5: 102000007,
|
|
619
|
+
6: 4,
|
|
620
|
+
7: 1,
|
|
621
|
+
13: 1,
|
|
622
|
+
14: encryptedOpenId,
|
|
623
|
+
15: region,
|
|
624
|
+
16: 1
|
|
625
|
+
};
|
|
626
|
+
const protoBytes = this._manualProtobufEncode(payload);
|
|
627
|
+
const { encrypt: encrypt2 } = await import("./crypto-UT7KAWHQ.mjs");
|
|
628
|
+
const encryptedBody = encrypt2(protoBytes);
|
|
629
|
+
try {
|
|
630
|
+
const response = await axios.post(URLS.MAJOR_REGISTER, encryptedBody, {
|
|
631
|
+
headers: {
|
|
632
|
+
Authorization: `Bearer ${accessToken}`,
|
|
633
|
+
"X-Unity-Version": "2018.4.11f1",
|
|
634
|
+
"X-GA": "v1 1",
|
|
635
|
+
ReleaseVersion: "OB53",
|
|
636
|
+
"Content-Type": "application/octet-stream",
|
|
637
|
+
"User-Agent": HEADERS.GARENA_AUTH["User-Agent"],
|
|
638
|
+
Host: "loginbp.ggblueshark.com",
|
|
639
|
+
Connection: "Keep-Alive",
|
|
640
|
+
"Accept-Encoding": "gzip"
|
|
641
|
+
},
|
|
642
|
+
responseType: "arraybuffer",
|
|
643
|
+
validateStatus: () => true,
|
|
644
|
+
timeout: 3e4
|
|
645
|
+
});
|
|
646
|
+
if (response.status === 200) return { success: true };
|
|
647
|
+
let errorDetail = `HTTP ${response.status}`;
|
|
648
|
+
try {
|
|
649
|
+
if (Buffer.isBuffer(response.data)) {
|
|
650
|
+
errorDetail += ` | Response: ${response.data.toString("hex").substring(0, 100)}`;
|
|
651
|
+
}
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
return { success: false, error: errorDetail };
|
|
655
|
+
} catch (error) {
|
|
656
|
+
return { success: false, error: `Request failed: ${getErrorMessage(error)}` };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
_manualProtobufDecode(buffer) {
|
|
660
|
+
const result = {};
|
|
661
|
+
let offset = 0;
|
|
662
|
+
while (offset < buffer.length) {
|
|
663
|
+
let tag = 0;
|
|
664
|
+
let shift = 0;
|
|
665
|
+
while (true) {
|
|
666
|
+
const byte = buffer[offset++];
|
|
667
|
+
tag |= (byte & 127) << shift;
|
|
668
|
+
shift += 7;
|
|
669
|
+
if ((byte & 128) === 0) break;
|
|
670
|
+
}
|
|
671
|
+
const fieldNum = tag >> 3;
|
|
672
|
+
const wireType = tag & 7;
|
|
673
|
+
if (wireType === 0) {
|
|
674
|
+
let value = 0;
|
|
675
|
+
shift = 0;
|
|
676
|
+
while (true) {
|
|
677
|
+
const byte = buffer[offset++];
|
|
678
|
+
value |= (byte & 127) << shift;
|
|
679
|
+
shift += 7;
|
|
680
|
+
if ((byte & 128) === 0) break;
|
|
681
|
+
}
|
|
682
|
+
result[fieldNum] = value;
|
|
683
|
+
} else if (wireType === 2) {
|
|
684
|
+
let length = 0;
|
|
685
|
+
shift = 0;
|
|
686
|
+
while (true) {
|
|
687
|
+
const byte = buffer[offset++];
|
|
688
|
+
length |= (byte & 127) << shift;
|
|
689
|
+
shift += 7;
|
|
690
|
+
if ((byte & 128) === 0) break;
|
|
691
|
+
}
|
|
692
|
+
result[fieldNum] = buffer.slice(offset, offset + length).toString("utf8");
|
|
693
|
+
offset += length;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return result;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// src/lib/like.ts
|
|
701
|
+
init_constants();
|
|
702
|
+
import axios2 from "axios";
|
|
703
|
+
var LikeAPI = class {
|
|
704
|
+
constructor() {
|
|
705
|
+
this.credentialManagers = {};
|
|
706
|
+
}
|
|
707
|
+
_getCredentialManager(region) {
|
|
708
|
+
if (!this.credentialManagers[region]) {
|
|
709
|
+
this.credentialManagers[region] = new CredentialManager(region);
|
|
710
|
+
}
|
|
711
|
+
return this.credentialManagers[region];
|
|
712
|
+
}
|
|
713
|
+
_getBaseUrl(region) {
|
|
714
|
+
const regionUpper = region.toUpperCase();
|
|
715
|
+
if (regionUpper === "IND") return "https://client.ind.freefiremobile.com";
|
|
716
|
+
if (["BR", "US", "SAC", "NA"].includes(regionUpper)) return "https://client.us.freefiremobile.com";
|
|
717
|
+
return "https://clientbp.ggblueshark.com";
|
|
718
|
+
}
|
|
719
|
+
async _login(uid, password) {
|
|
720
|
+
try {
|
|
721
|
+
const params = new URLSearchParams();
|
|
722
|
+
params.append("uid", uid);
|
|
723
|
+
params.append("password", password);
|
|
724
|
+
params.append("response_type", "token");
|
|
725
|
+
params.append("client_type", "2");
|
|
726
|
+
params.append("client_secret", GARENA_CLIENT.CLIENT_SECRET);
|
|
727
|
+
params.append("client_id", GARENA_CLIENT.CLIENT_ID);
|
|
728
|
+
const tokenResponse = await axios2.post(URLS.GARENA_TOKEN, params, { headers: HEADERS.GARENA_AUTH, timeout: 3e4 });
|
|
729
|
+
if (!tokenResponse.data?.access_token) return null;
|
|
730
|
+
const accessToken = tokenResponse.data.access_token;
|
|
731
|
+
const openId = tokenResponse.data.open_id;
|
|
732
|
+
const loginPayload = { openid: openId, logintoken: accessToken, platform: "4" };
|
|
733
|
+
const encryptedBody = await protoHandler.encode("MajorLogin.proto", "request", loginPayload, true);
|
|
734
|
+
const loginResponse = await axios2.post(URLS.MAJOR_LOGIN, encryptedBody, {
|
|
735
|
+
headers: {
|
|
736
|
+
...HEADERS.COMMON,
|
|
737
|
+
Authorization: "Bearer",
|
|
738
|
+
"Content-Type": "application/octet-stream"
|
|
739
|
+
},
|
|
740
|
+
responseType: "arraybuffer",
|
|
741
|
+
timeout: 3e4
|
|
742
|
+
});
|
|
743
|
+
const loginData = await protoHandler.decode("MajorLogin.proto", "response", loginResponse.data);
|
|
744
|
+
if (loginData && typeof loginData === "object" && "token" in loginData) {
|
|
745
|
+
const decoded = loginData;
|
|
746
|
+
return {
|
|
747
|
+
jwt: decoded.token,
|
|
748
|
+
serverUrl: decoded.serverUrl || "",
|
|
749
|
+
accountId: decoded.accountid
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
return null;
|
|
753
|
+
} catch (error) {
|
|
754
|
+
console.log(`[LikeAPI] Login error: ${getErrorMessage(error)}`);
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
_createLikePayload(targetUid, region) {
|
|
759
|
+
const fields = [];
|
|
760
|
+
const targetBytes = Buffer.from(targetUid, "utf8");
|
|
761
|
+
fields.push(Buffer.concat([Buffer.from([10, targetBytes.length]), targetBytes]));
|
|
762
|
+
const regionBytes = Buffer.from(region, "utf8");
|
|
763
|
+
fields.push(Buffer.concat([Buffer.from([18, regionBytes.length]), regionBytes]));
|
|
764
|
+
const payload = Buffer.concat(fields);
|
|
765
|
+
const { encrypt: encrypt2 } = (init_crypto(), __toCommonJS(crypto_exports));
|
|
766
|
+
return encrypt2(payload);
|
|
767
|
+
}
|
|
768
|
+
async _sendLikeWithGuest(guest, targetUid, region) {
|
|
769
|
+
try {
|
|
770
|
+
const auth = await this._login(guest.uid, guest.password);
|
|
771
|
+
if (!auth) return { success: false, error: "Login failed" };
|
|
772
|
+
const serverUrl = auth.serverUrl || this._getBaseUrl(region);
|
|
773
|
+
const payload = this._createLikePayload(targetUid, region);
|
|
774
|
+
const headers = {
|
|
775
|
+
"User-Agent": HEADERS.COMMON["User-Agent"],
|
|
776
|
+
Connection: HEADERS.COMMON["Connection"],
|
|
777
|
+
"Accept-Encoding": HEADERS.COMMON["Accept-Encoding"],
|
|
778
|
+
"Content-Type": "application/octet-stream",
|
|
779
|
+
Expect: HEADERS.COMMON["Expect"],
|
|
780
|
+
Authorization: `Bearer ${auth.jwt}`,
|
|
781
|
+
"X-Unity-Version": HEADERS.COMMON["X-Unity-Version"],
|
|
782
|
+
"X-GA": HEADERS.COMMON["X-GA"],
|
|
783
|
+
ReleaseVersion: HEADERS.COMMON["ReleaseVersion"]
|
|
784
|
+
};
|
|
785
|
+
const response = await axios2.post(`${serverUrl}/LikeProfile`, payload, {
|
|
786
|
+
headers,
|
|
787
|
+
timeout: 3e4,
|
|
788
|
+
responseType: "arraybuffer"
|
|
789
|
+
});
|
|
790
|
+
if (response.status === 200) return { success: true };
|
|
791
|
+
return { success: false, error: `HTTP ${response.status}` };
|
|
792
|
+
} catch (error) {
|
|
793
|
+
return { success: false, error: getErrorMessage(error) };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Sends likes to a target player using available guest accounts.
|
|
798
|
+
* @param targetUid - UID of the player to receive likes.
|
|
799
|
+
* @param region - Region code (e.g., 'IND', 'BR').
|
|
800
|
+
* @param likeCount - Number of likes to send (default 100, max 100 per day).
|
|
801
|
+
* @returns Summary of the like operation including success and failure counts.
|
|
802
|
+
*/
|
|
803
|
+
async sendLikes(targetUid, region, likeCount = 100) {
|
|
804
|
+
const cm = this._getCredentialManager(region);
|
|
805
|
+
const availableCount = cm.getAvailableCount(targetUid);
|
|
806
|
+
console.log(`[LikeAPI] Available guests for ${targetUid}: ${availableCount}/${cm.getPoolSize()}`);
|
|
807
|
+
const maxDaily = 100;
|
|
808
|
+
const requestedLikes = Math.min(likeCount, maxDaily);
|
|
809
|
+
const plannedLikes = Math.min(requestedLikes, availableCount);
|
|
810
|
+
if (plannedLikes === 0) {
|
|
811
|
+
return {
|
|
812
|
+
success: false,
|
|
813
|
+
message: "No available guests left for this target. All guests have been used.",
|
|
814
|
+
successCount: 0,
|
|
815
|
+
failedCount: 0,
|
|
816
|
+
remainingGuests: 0
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
console.log(`[LikeAPI] Planning to send ${plannedLikes} likes to ${targetUid} using ${region} guests`);
|
|
820
|
+
const guests = cm.getMultipleForTarget(targetUid, plannedLikes);
|
|
821
|
+
let successCount = 0;
|
|
822
|
+
let failedCount = 0;
|
|
823
|
+
for (let i = 0; i < guests.length; i++) {
|
|
824
|
+
const guest = guests[i];
|
|
825
|
+
process.stdout.write(`[LikeAPI] Progress: ${i + 1}/${guests.length} (${successCount}\u2713 ${failedCount}\u2717)\r`);
|
|
826
|
+
const result = await this._sendLikeWithGuest(guest, targetUid, region);
|
|
827
|
+
if (result.success) {
|
|
828
|
+
successCount++;
|
|
829
|
+
cm.markUsed(targetUid, guest.uid);
|
|
830
|
+
} else {
|
|
831
|
+
failedCount++;
|
|
832
|
+
console.log(`
|
|
833
|
+
[LikeAPI] Guest ${guest.uid} failed: ${result.error}`);
|
|
834
|
+
}
|
|
835
|
+
if (i < guests.length - 1) {
|
|
836
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
console.log(`
|
|
840
|
+
[LikeAPI] Completed: ${successCount}/${guests.length} likes sent successfully`);
|
|
841
|
+
return {
|
|
842
|
+
success: successCount > 0,
|
|
843
|
+
successCount,
|
|
844
|
+
failedCount,
|
|
845
|
+
remainingGuests: cm.getAvailableCount(targetUid),
|
|
846
|
+
message: `Sent ${successCount} likes to ${targetUid}. ${cm.getAvailableCount(targetUid)} guests remaining.`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/index.ts
|
|
852
|
+
init_crypto();
|
|
853
|
+
|
|
854
|
+
// src/lib/ai-tools.ts
|
|
855
|
+
var freefireTools = [
|
|
856
|
+
{
|
|
857
|
+
type: "function",
|
|
858
|
+
function: {
|
|
859
|
+
name: "search_player",
|
|
860
|
+
description: "Search Free Fire players by nickname. Returns matching players with their account ID, nickname, and level.",
|
|
861
|
+
parameters: {
|
|
862
|
+
type: "object",
|
|
863
|
+
properties: {
|
|
864
|
+
keyword: {
|
|
865
|
+
type: "string",
|
|
866
|
+
description: "Player nickname to search. Minimum 3 characters.",
|
|
867
|
+
minLength: 3
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
required: ["keyword"]
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
type: "function",
|
|
876
|
+
function: {
|
|
877
|
+
name: "get_player_profile",
|
|
878
|
+
description: "Get detailed profile information for a Free Fire player including basic info, clan, pet, and equipment.",
|
|
879
|
+
parameters: {
|
|
880
|
+
type: "object",
|
|
881
|
+
properties: {
|
|
882
|
+
uid: {
|
|
883
|
+
type: "string",
|
|
884
|
+
description: "Target player UID (account ID)."
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
required: ["uid"]
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
type: "function",
|
|
893
|
+
function: {
|
|
894
|
+
name: "get_player_items",
|
|
895
|
+
description: "Get a player's equipped items including outfit, weapons skins, skills, and pet details with metadata from the items database.",
|
|
896
|
+
parameters: {
|
|
897
|
+
type: "object",
|
|
898
|
+
properties: {
|
|
899
|
+
uid: {
|
|
900
|
+
type: "string",
|
|
901
|
+
description: "Target player UID (account ID)."
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
required: ["uid"]
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
type: "function",
|
|
910
|
+
function: {
|
|
911
|
+
name: "get_player_stats",
|
|
912
|
+
description: "Retrieve match statistics for a player. Supports Battle Royale (BR) and Clash Squad (CS) modes.",
|
|
913
|
+
parameters: {
|
|
914
|
+
type: "object",
|
|
915
|
+
properties: {
|
|
916
|
+
uid: {
|
|
917
|
+
type: "string",
|
|
918
|
+
description: "Target player UID (account ID)."
|
|
919
|
+
},
|
|
920
|
+
mode: {
|
|
921
|
+
type: "string",
|
|
922
|
+
description: "Game mode: br (Battle Royale) or cs (Clash Squad). Defaults to br.",
|
|
923
|
+
enum: ["br", "cs"]
|
|
924
|
+
},
|
|
925
|
+
matchType: {
|
|
926
|
+
type: "string",
|
|
927
|
+
description: "Match type: career, ranked, or normal. Defaults to career.",
|
|
928
|
+
enum: ["career", "ranked", "normal"]
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
required: ["uid"]
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
type: "function",
|
|
937
|
+
function: {
|
|
938
|
+
name: "send_likes",
|
|
939
|
+
description: "Send profile likes to a target player using available guest accounts. Maximum 100 likes per day per target.",
|
|
940
|
+
parameters: {
|
|
941
|
+
type: "object",
|
|
942
|
+
properties: {
|
|
943
|
+
targetUid: {
|
|
944
|
+
type: "string",
|
|
945
|
+
description: "UID of the player to receive likes."
|
|
946
|
+
},
|
|
947
|
+
region: {
|
|
948
|
+
type: "string",
|
|
949
|
+
description: "Region code (e.g., IND, BR, US)."
|
|
950
|
+
},
|
|
951
|
+
likeCount: {
|
|
952
|
+
type: "number",
|
|
953
|
+
description: "Number of likes to send. Defaults to 100, max 100.",
|
|
954
|
+
minimum: 1,
|
|
955
|
+
maximum: 100
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
required: ["targetUid", "region"]
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
type: "function",
|
|
964
|
+
function: {
|
|
965
|
+
name: "register_account",
|
|
966
|
+
description: "Register a new guest Free Fire account in the specified region.",
|
|
967
|
+
parameters: {
|
|
968
|
+
type: "object",
|
|
969
|
+
properties: {
|
|
970
|
+
region: {
|
|
971
|
+
type: "string",
|
|
972
|
+
description: "Target region code (e.g., IND, BR)."
|
|
973
|
+
},
|
|
974
|
+
nickname: {
|
|
975
|
+
type: "string",
|
|
976
|
+
description: "Optional nickname. A random one is generated if omitted."
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
required: ["region"]
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
];
|
|
984
|
+
function getToolByName(name) {
|
|
985
|
+
return freefireTools.find((t) => t.function.name === name);
|
|
986
|
+
}
|
|
987
|
+
function getToolNames() {
|
|
988
|
+
return freefireTools.map((t) => t.function.name);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// src/lib/ai-handler.ts
|
|
992
|
+
var FreeFireAIToolHandler = class {
|
|
993
|
+
constructor(options = {}) {
|
|
994
|
+
this.api = new FreeFireAPI(options.region || null);
|
|
995
|
+
this.likeApi = new LikeAPI();
|
|
996
|
+
}
|
|
997
|
+
async execute(toolCall) {
|
|
998
|
+
const tool = getToolByName(toolCall.function.name);
|
|
999
|
+
if (!tool) {
|
|
1000
|
+
return this._buildResult(toolCall, `Error: Unknown tool "${toolCall.function.name}"`);
|
|
1001
|
+
}
|
|
1002
|
+
let args;
|
|
1003
|
+
try {
|
|
1004
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
1005
|
+
} catch {
|
|
1006
|
+
return this._buildResult(toolCall, "Error: Invalid JSON in tool arguments");
|
|
1007
|
+
}
|
|
1008
|
+
try {
|
|
1009
|
+
const result = await this._dispatch(toolCall.function.name, args);
|
|
1010
|
+
return this._buildResult(toolCall, JSON.stringify(result));
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1013
|
+
return this._buildResult(toolCall, `Error: ${message}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
async executeMany(toolCalls) {
|
|
1017
|
+
const results = [];
|
|
1018
|
+
for (const call of toolCalls) {
|
|
1019
|
+
results.push(await this.execute(call));
|
|
1020
|
+
}
|
|
1021
|
+
return results;
|
|
1022
|
+
}
|
|
1023
|
+
async _dispatch(name, args) {
|
|
1024
|
+
switch (name) {
|
|
1025
|
+
case "search_player": {
|
|
1026
|
+
const keyword = String(args.keyword);
|
|
1027
|
+
const results = await this.api.searchAccount(keyword);
|
|
1028
|
+
return results.map((r) => ({
|
|
1029
|
+
accountid: r.accountid,
|
|
1030
|
+
nickname: r.nickname,
|
|
1031
|
+
level: r.level
|
|
1032
|
+
}));
|
|
1033
|
+
}
|
|
1034
|
+
case "get_player_profile": {
|
|
1035
|
+
const profile = await this.api.getPlayerProfile(String(args.uid));
|
|
1036
|
+
return this._sanitizeProfile(profile);
|
|
1037
|
+
}
|
|
1038
|
+
case "get_player_items": {
|
|
1039
|
+
const items = await this.api.getPlayerItems(String(args.uid));
|
|
1040
|
+
return items;
|
|
1041
|
+
}
|
|
1042
|
+
case "get_player_stats": {
|
|
1043
|
+
const stats = await this.api.getPlayerStats(
|
|
1044
|
+
String(args.uid),
|
|
1045
|
+
args.mode || "br",
|
|
1046
|
+
args.matchType || "career"
|
|
1047
|
+
);
|
|
1048
|
+
return stats;
|
|
1049
|
+
}
|
|
1050
|
+
case "send_likes": {
|
|
1051
|
+
const result = await this.likeApi.sendLikes(
|
|
1052
|
+
String(args.targetUid),
|
|
1053
|
+
String(args.region),
|
|
1054
|
+
args.likeCount ? Number(args.likeCount) : 100
|
|
1055
|
+
);
|
|
1056
|
+
return this._sanitizeLikeResult(result);
|
|
1057
|
+
}
|
|
1058
|
+
case "register_account": {
|
|
1059
|
+
const result = await this.api.register(
|
|
1060
|
+
String(args.region),
|
|
1061
|
+
args.nickname ? String(args.nickname) : null
|
|
1062
|
+
);
|
|
1063
|
+
return this._sanitizeRegisterResult(result);
|
|
1064
|
+
}
|
|
1065
|
+
default:
|
|
1066
|
+
throw new Error(`Tool "${name}" is not implemented`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
_buildResult(toolCall, content) {
|
|
1070
|
+
return {
|
|
1071
|
+
tool_call_id: toolCall.id,
|
|
1072
|
+
role: "tool",
|
|
1073
|
+
name: toolCall.function.name,
|
|
1074
|
+
content
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
_sanitizeProfile(profile) {
|
|
1078
|
+
return {
|
|
1079
|
+
basicinfo: profile.basicinfo,
|
|
1080
|
+
claninfo: profile.claninfo || null,
|
|
1081
|
+
petinfo: profile.petinfo ? {
|
|
1082
|
+
id: profile.petinfo.id,
|
|
1083
|
+
name: profile.petinfo.name,
|
|
1084
|
+
level: profile.petinfo.level
|
|
1085
|
+
} : null,
|
|
1086
|
+
profileinfo: profile.profileinfo
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
_sanitizeLikeResult(result) {
|
|
1090
|
+
return {
|
|
1091
|
+
success: result.success,
|
|
1092
|
+
successCount: result.successCount,
|
|
1093
|
+
failedCount: result.failedCount,
|
|
1094
|
+
remainingGuests: result.remainingGuests,
|
|
1095
|
+
message: result.message
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
_sanitizeRegisterResult(result) {
|
|
1099
|
+
return {
|
|
1100
|
+
uid: result.uid,
|
|
1101
|
+
region: result.region,
|
|
1102
|
+
nickname: result.nickname
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
export {
|
|
1107
|
+
CredentialManager,
|
|
1108
|
+
FreeFireAIToolHandler,
|
|
1109
|
+
FreeFireAPI,
|
|
1110
|
+
LikeAPI,
|
|
1111
|
+
encrypt,
|
|
1112
|
+
freefireTools,
|
|
1113
|
+
getErrorMessage,
|
|
1114
|
+
getItemDetails,
|
|
1115
|
+
getToolByName,
|
|
1116
|
+
getToolNames,
|
|
1117
|
+
loadItems,
|
|
1118
|
+
processPlayerItems,
|
|
1119
|
+
protoHandler
|
|
1120
|
+
};
|