@nekosuneprojects/warframe-api 1.0.0 → 1.0.1
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 +21 -21
- package/README.md +298 -298
- package/data/characters/amesha.json +226 -226
- package/data/characters/ash.json +1354 -1354
- package/data/characters/ashprime.json +1994 -1994
- package/data/characters/atlas.json +916 -916
- package/data/characters/atlasprime.json +1632 -1632
- package/data/characters/banshee.json +1036 -1036
- package/data/characters/bansheeprime.json +1750 -1750
- package/data/characters/baruuk.json +555 -555
- package/data/characters/baruukprime.json +1298 -1298
- package/data/characters/bonewidow.json +354 -354
- package/data/characters/caliban.json +434 -434
- package/data/characters/calibanprime.json +683 -683
- package/data/characters/chroma.json +1344 -1344
- package/data/characters/chromaprime.json +1977 -1977
- package/data/characters/citrine.json +220 -220
- package/data/characters/citrineprime.json +62 -62
- package/data/characters/cyte09.json +218 -218
- package/data/characters/dagath.json +194 -194
- package/data/characters/dante.json +223 -223
- package/data/characters/elytron.json +300 -300
- package/data/characters/ember.json +1209 -1209
- package/data/characters/emberprime.json +1678 -1678
- package/data/characters/equinox.json +958 -958
- package/data/characters/equinoxprime.json +1644 -1644
- package/data/characters/excalibur.json +1603 -1603
- package/data/characters/excaliburprime.json +1524 -1524
- package/data/characters/excaliburumbra.json +397 -397
- package/data/characters/follie.json +246 -246
- package/data/characters/frost.json +1330 -1330
- package/data/characters/frostprime.json +1686 -1686
- package/data/characters/gara.json +925 -925
- package/data/characters/garaprime.json +1494 -1494
- package/data/characters/garuda.json +851 -851
- package/data/characters/garudaprime.json +1449 -1449
- package/data/characters/gauss.json +506 -506
- package/data/characters/gaussprime.json +1077 -1077
- package/data/characters/grendel.json +463 -463
- package/data/characters/grendelprime.json +772 -772
- package/data/characters/gyre.json +381 -381
- package/data/characters/gyreprime.json +403 -403
- package/data/characters/harrow.json +916 -916
- package/data/characters/harrowprime.json +1700 -1700
- package/data/characters/helminth.json +220 -220
- package/data/characters/hildryn.json +627 -627
- package/data/characters/hildrynprime.json +996 -996
- package/data/characters/hydroid.json +1028 -1028
- package/data/characters/hydroidprime.json +1621 -1621
- package/data/characters/inaros.json +976 -976
- package/data/characters/inarosprime.json +1721 -1721
- package/data/characters/itzal.json +465 -465
- package/data/characters/ivara.json +1349 -1349
- package/data/characters/ivaraprime.json +1921 -1921
- package/data/characters/jade.json +240 -240
- package/data/characters/khora.json +804 -804
- package/data/characters/khoraprime.json +1403 -1403
- package/data/characters/koumei.json +221 -221
- package/data/characters/kullervo.json +191 -191
- package/data/characters/lavos.json +511 -511
- package/data/characters/lavosprime.json +711 -711
- package/data/characters/limbo.json +1363 -1363
- package/data/characters/limboprime.json +2084 -2084
- package/data/characters/loki.json +1209 -1209
- package/data/characters/lokiprime.json +1457 -1457
- package/data/characters/mag.json +2089 -2089
- package/data/characters/magprime.json +2532 -2532
- package/data/characters/mesa.json +1195 -1195
- package/data/characters/mesaprime.json +2059 -2059
- package/data/characters/mirage.json +1561 -1561
- package/data/characters/mirageprime.json +2257 -2257
- package/data/characters/narin.json +43 -43
- package/data/characters/nekros.json +1235 -1235
- package/data/characters/nekrosprime.json +1882 -1882
- package/data/characters/nezha.json +868 -868
- package/data/characters/nezhaprime.json +1695 -1695
- package/data/characters/nidus.json +1027 -1027
- package/data/characters/nidusprime.json +1706 -1706
- package/data/characters/nokko.json +308 -308
- package/data/characters/nova.json +1226 -1226
- package/data/characters/novaprime.json +1667 -1667
- package/data/characters/nyx.json +1610 -1610
- package/data/characters/nyxprime.json +1967 -1967
- package/data/characters/oberon.json +1249 -1249
- package/data/characters/oberonprime.json +1872 -1872
- package/data/characters/octavia.json +1049 -1049
- package/data/characters/octaviaprime.json +1735 -1735
- package/data/characters/odonata.json +336 -336
- package/data/characters/odonataprime.json +787 -787
- package/data/characters/oraxia.json +246 -246
- package/data/characters/orionsirius.json +134 -134
- package/data/characters/protea.json +521 -521
- package/data/characters/proteaprime.json +992 -992
- package/data/characters/qorvex.json +214 -214
- package/data/characters/revenant.json +931 -931
- package/data/characters/revenantprime.json +1494 -1494
- package/data/characters/rhino.json +1344 -1344
- package/data/characters/rhinoprime.json +1675 -1675
- package/data/characters/saryn.json +1210 -1210
- package/data/characters/sarynprime.json +2042 -2042
- package/data/characters/sevagoth.json +478 -478
- package/data/characters/sevagothprime.json +910 -910
- package/data/characters/siriusorion.json +247 -247
- package/data/characters/styanax.json +320 -320
- package/data/characters/styanaxprime.json +321 -321
- package/data/characters/temple.json +271 -271
- package/data/characters/titania.json +1227 -1227
- package/data/characters/titaniaprime.json +2000 -2000
- package/data/characters/trinity.json +1097 -1097
- package/data/characters/trinityprime.json +1849 -1849
- package/data/characters/uriel.json +215 -215
- package/data/characters/valkyr.json +1165 -1165
- package/data/characters/valkyrprime.json +1969 -1969
- package/data/characters/vauban.json +1067 -1067
- package/data/characters/vaubanprime.json +1614 -1614
- package/data/characters/voidrig.json +355 -355
- package/data/characters/volt.json +1324 -1324
- package/data/characters/voltprime.json +1843 -1843
- package/data/characters/voruna.json +293 -293
- package/data/characters/vorunaprime.json +431 -431
- package/data/characters/wisp.json +824 -824
- package/data/characters/wispprime.json +908 -908
- package/data/characters/wukong.json +927 -927
- package/data/characters/wukongprime.json +1754 -1754
- package/data/characters/xaku.json +620 -620
- package/data/characters/xakuprime.json +879 -879
- package/data/characters/yareli.json +409 -409
- package/data/characters/yareliprime.json +683 -683
- package/data/characters/zephyr.json +1149 -1149
- package/data/characters/zephyrprime.json +1864 -1864
- package/data/codes/warframe.json +20824 -20824
- package/data/meta.json +14 -14
- package/index.js +11 -11
- package/package.json +50 -50
- package/src/catalog.js +105 -105
- package/src/client-error.js +9 -9
- package/src/data-source.js +7 -7
- package/src/data-store.js +138 -138
- package/src/provider.js +22 -22
- package/src/tracker.js +115 -115
- package/src/warframe-client.js +57 -57
- package/src/warframe-codes.js +82 -82
- package/src/wfcd.js +93 -93
package/src/warframe-client.js
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const { TrackerError } = require('./client-error');
|
|
3
|
-
const provider = require('./provider');
|
|
4
|
-
|
|
5
|
-
function parameter(value, name) {
|
|
6
|
-
if (typeof value !== 'string' || !value.trim()) throw new TypeError(name + ' must be nonempty text');
|
|
7
|
-
return encodeURIComponent(value.trim());
|
|
8
|
-
}
|
|
9
|
-
function optionsQuery(options, allowed) {
|
|
10
|
-
if (options === undefined || options === null) return undefined;
|
|
11
|
-
if (typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Options must be an object');
|
|
12
|
-
const params = {};
|
|
13
|
-
for (const [key,value] of Object.entries(options)) {
|
|
14
|
-
if (!allowed.includes(key)) throw new TypeError('Unsupported query option: ' + key);
|
|
15
|
-
if (value !== undefined && value !== null) {
|
|
16
|
-
if (!['string','number','boolean'].includes(typeof value)) throw new TypeError('Invalid query option: ' + key);
|
|
17
|
-
params[key]=String(value);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return params;
|
|
21
|
-
}
|
|
22
|
-
function createWarframeClient({ timeoutMs = 15000, fetchImpl = globalThis.fetch, language = 'en' } = {}) {
|
|
23
|
-
if (typeof fetchImpl !== 'function') throw new TypeError('Node.js 18+ fetch is required');
|
|
24
|
-
if (!Number.isInteger(timeoutMs) || timeoutMs < 1) throw new TypeError('timeoutMs must be a positive integer');
|
|
25
|
-
if (typeof language !== 'string' || !/^[a-z]{2}$/.test(language)) throw new TypeError('language must be a 2-letter code');
|
|
26
|
-
async function request(route, query, { signal } = {}) {
|
|
27
|
-
if (typeof route !== 'string' || !/^[a-zA-Z0-9_/%.-]+$/.test(route) || route.startsWith('/'))
|
|
28
|
-
throw new TypeError('Invalid internal API route');
|
|
29
|
-
const url = new URL(route, provider.baseUrl);
|
|
30
|
-
if (url.origin !== new URL(provider.baseUrl).origin) throw new TypeError('API origin mismatch');
|
|
31
|
-
for (const [k,v] of Object.entries(query || {})) url.searchParams.set(k,String(v));
|
|
32
|
-
const controller = new AbortController();
|
|
33
|
-
const timer = setTimeout(() => controller.abort(),timeoutMs);
|
|
34
|
-
const onAbort = () => controller.abort();
|
|
35
|
-
if (signal) {
|
|
36
|
-
if (signal.aborted) controller.abort();
|
|
37
|
-
else signal.addEventListener('abort',onAbort,{once:true});
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
const response = await fetchImpl(url,{
|
|
41
|
-
headers:{Accept:'application/json','Accept-Language':language,'User-Agent':'nekosuneprojects-warframe-api'},
|
|
42
|
-
signal:controller.signal
|
|
43
|
-
});
|
|
44
|
-
if (!response.ok) throw new TrackerError('WarframeStat.us HTTP '+response.status,{status:response.status});
|
|
45
|
-
if (response.status===204) return null;
|
|
46
|
-
return await response.json();
|
|
47
|
-
} catch (cause) {
|
|
48
|
-
if (cause instanceof TrackerError) throw cause;
|
|
49
|
-
throw new TrackerError('WarframeStat.us request failed',{cause});
|
|
50
|
-
} finally {
|
|
51
|
-
clearTimeout(timer);
|
|
52
|
-
if (signal) signal.removeEventListener('abort',onAbort);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return Object.freeze({request,parameter,optionsQuery});
|
|
56
|
-
}
|
|
57
|
-
module.exports={createWarframeClient,parameter,optionsQuery};
|
|
1
|
+
'use strict';
|
|
2
|
+
const { TrackerError } = require('./client-error');
|
|
3
|
+
const provider = require('./provider');
|
|
4
|
+
|
|
5
|
+
function parameter(value, name) {
|
|
6
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(name + ' must be nonempty text');
|
|
7
|
+
return encodeURIComponent(value.trim());
|
|
8
|
+
}
|
|
9
|
+
function optionsQuery(options, allowed) {
|
|
10
|
+
if (options === undefined || options === null) return undefined;
|
|
11
|
+
if (typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Options must be an object');
|
|
12
|
+
const params = {};
|
|
13
|
+
for (const [key,value] of Object.entries(options)) {
|
|
14
|
+
if (!allowed.includes(key)) throw new TypeError('Unsupported query option: ' + key);
|
|
15
|
+
if (value !== undefined && value !== null) {
|
|
16
|
+
if (!['string','number','boolean'].includes(typeof value)) throw new TypeError('Invalid query option: ' + key);
|
|
17
|
+
params[key]=String(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return params;
|
|
21
|
+
}
|
|
22
|
+
function createWarframeClient({ timeoutMs = 15000, fetchImpl = globalThis.fetch, language = 'en' } = {}) {
|
|
23
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('Node.js 18+ fetch is required');
|
|
24
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1) throw new TypeError('timeoutMs must be a positive integer');
|
|
25
|
+
if (typeof language !== 'string' || !/^[a-z]{2}$/.test(language)) throw new TypeError('language must be a 2-letter code');
|
|
26
|
+
async function request(route, query, { signal } = {}) {
|
|
27
|
+
if (typeof route !== 'string' || !/^[a-zA-Z0-9_/%.-]+$/.test(route) || route.startsWith('/'))
|
|
28
|
+
throw new TypeError('Invalid internal API route');
|
|
29
|
+
const url = new URL(route, provider.baseUrl);
|
|
30
|
+
if (url.origin !== new URL(provider.baseUrl).origin) throw new TypeError('API origin mismatch');
|
|
31
|
+
for (const [k,v] of Object.entries(query || {})) url.searchParams.set(k,String(v));
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => controller.abort(),timeoutMs);
|
|
34
|
+
const onAbort = () => controller.abort();
|
|
35
|
+
if (signal) {
|
|
36
|
+
if (signal.aborted) controller.abort();
|
|
37
|
+
else signal.addEventListener('abort',onAbort,{once:true});
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetchImpl(url,{
|
|
41
|
+
headers:{Accept:'application/json','Accept-Language':language,'User-Agent':'nekosuneprojects-warframe-api'},
|
|
42
|
+
signal:controller.signal
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) throw new TrackerError('WarframeStat.us HTTP '+response.status,{status:response.status});
|
|
45
|
+
if (response.status===204) return null;
|
|
46
|
+
return await response.json();
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
if (cause instanceof TrackerError) throw cause;
|
|
49
|
+
throw new TrackerError('WarframeStat.us request failed',{cause});
|
|
50
|
+
} finally {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
if (signal) signal.removeEventListener('abort',onAbort);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Object.freeze({request,parameter,optionsQuery});
|
|
56
|
+
}
|
|
57
|
+
module.exports={createWarframeClient,parameter,optionsQuery};
|
package/src/warframe-codes.js
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const { createDataStore } = require('./data-store');
|
|
4
|
-
|
|
5
|
-
const FILE = 'warframe.json';
|
|
6
|
-
const GROUPS = Object.freeze({ active:'Active', expired:'Expired', unknown:'Unknown' });
|
|
7
|
-
const normalise = text => String(text ?? '').normalize('NFKC').trim().toLowerCase();
|
|
8
|
-
function validate(source) {
|
|
9
|
-
if (!source || typeof source !== 'object' || Array.isArray(source) ||
|
|
10
|
-
!Object.values(GROUPS).every(group => Array.isArray(source[group]))) {
|
|
11
|
-
throw new TypeError('Invalid Warframe codes JSON: Active, Expired and Unknown arrays are required');
|
|
12
|
-
}
|
|
13
|
-
return source;
|
|
14
|
-
}
|
|
15
|
-
function createWarframeCodes({cacheDir,githubToken,fetchImpl}={}) {
|
|
16
|
-
const storage = createDataStore({
|
|
17
|
-
gameId:'warframe-codes',
|
|
18
|
-
cacheDir:cacheDir ? path.join(path.resolve(cacheDir),'codes') : undefined,
|
|
19
|
-
bundledDir:path.join(__dirname,'..','data','codes'),
|
|
20
|
-
github:{owner:'NekoSuneProjectsNPM',repo:'game-tracker',branch:'JsonData',directory:'codes/games'},
|
|
21
|
-
syncFiles:[FILE],
|
|
22
|
-
githubToken,fetchImpl
|
|
23
|
-
});
|
|
24
|
-
async function raw() { return validate(await storage.get(FILE)); }
|
|
25
|
-
async function list({status,category,claimType,hasCode,limit}={}) {
|
|
26
|
-
if (status !== undefined && !Object.hasOwn(GROUPS,status)) throw new RangeError('status must be active, expired, or unknown');
|
|
27
|
-
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) throw new RangeError('limit must be a positive integer');
|
|
28
|
-
const doc=await raw();
|
|
29
|
-
const keys=status ? [GROUPS[status]] : Object.values(GROUPS);
|
|
30
|
-
let entries=keys.flatMap(key=>doc[key]);
|
|
31
|
-
if (category !== undefined) entries=entries.filter(item=>normalise(item.category)===normalise(category));
|
|
32
|
-
if (claimType !== undefined) entries=entries.filter(item=>normalise(item.claimType)===normalise(claimType));
|
|
33
|
-
if (hasCode !== undefined) {
|
|
34
|
-
if (typeof hasCode !== 'boolean') throw new TypeError('hasCode must be boolean');
|
|
35
|
-
entries=entries.filter(item=>Boolean(typeof item.code==='string'&&item.code.trim())===hasCode);
|
|
36
|
-
}
|
|
37
|
-
return limit ? entries.slice(0,limit) : entries;
|
|
38
|
-
}
|
|
39
|
-
async function search(query,options={}) {
|
|
40
|
-
if(typeof query!=='string'||!query.trim()) throw new TypeError('Search query is required');
|
|
41
|
-
const needle=normalise(query);
|
|
42
|
-
const entries=await list(options);
|
|
43
|
-
const results=entries.filter(record=>{
|
|
44
|
-
const fields=[record.code,record.id,record.category,record.claimType,record.notes,record.redeemLocation,
|
|
45
|
-
...(Array.isArray(record.items)?record.items.map(item=>item?.name):[])];
|
|
46
|
-
return fields.some(value=>normalise(value).includes(needle));
|
|
47
|
-
});
|
|
48
|
-
return {query,total:results.length,results,message:results.length?
|
|
49
|
-
'Found '+results.length+' Warframe code-related entries for "'+query+'".':
|
|
50
|
-
'We don\'t have anything related to "'+query+'" in Warframe codes.'};
|
|
51
|
-
}
|
|
52
|
-
async function get(key) {
|
|
53
|
-
if((typeof key!=='string' && typeof key!=='number') || !String(key).trim()) throw new TypeError('Code or ID is required');
|
|
54
|
-
const needle=normalise(key);
|
|
55
|
-
const results=(await list()).filter(item=>normalise(item.id)===needle || (item.code && normalise(item.code)===needle));
|
|
56
|
-
if(results.length===0) {
|
|
57
|
-
const err=new Error('No Warframe code entry found: '+key); err.code='NOT_FOUND';throw err;
|
|
58
|
-
}
|
|
59
|
-
if(results.length>1) {
|
|
60
|
-
const err=new Error('Multiple Warframe code entries match: '+key);err.code='AMBIGUOUS';err.results=results;throw err;
|
|
61
|
-
}
|
|
62
|
-
return results[0];
|
|
63
|
-
}
|
|
64
|
-
async function summary() {
|
|
65
|
-
const doc=await raw(); const groups={};let total=0;let redeemable=0;
|
|
66
|
-
for(const [status,key] of Object.entries(GROUPS)) {
|
|
67
|
-
const entries=doc[key];
|
|
68
|
-
const withCode=entries.filter(item=>typeof item.code==='string' && item.code.trim()).length;
|
|
69
|
-
groups[status]={total:entries.length,withCode,withoutCode:entries.length-withCode};
|
|
70
|
-
total+=entries.length;redeemable+=withCode;
|
|
71
|
-
}
|
|
72
|
-
return {total,withCode:redeemable,withoutCode:total-redeemable,groups};
|
|
73
|
-
}
|
|
74
|
-
return Object.freeze({
|
|
75
|
-
raw,list,active:options=>list({...options,status:'active'}),
|
|
76
|
-
expired:options=>list({...options,status:'expired'}),
|
|
77
|
-
unknown:options=>list({...options,status:'unknown'}),
|
|
78
|
-
search,get,summary,sync:options=>storage.sync(options),
|
|
79
|
-
cacheDir:storage.cacheDir
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
module.exports={createWarframeCodes};
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { createDataStore } = require('./data-store');
|
|
4
|
+
|
|
5
|
+
const FILE = 'warframe.json';
|
|
6
|
+
const GROUPS = Object.freeze({ active:'Active', expired:'Expired', unknown:'Unknown' });
|
|
7
|
+
const normalise = text => String(text ?? '').normalize('NFKC').trim().toLowerCase();
|
|
8
|
+
function validate(source) {
|
|
9
|
+
if (!source || typeof source !== 'object' || Array.isArray(source) ||
|
|
10
|
+
!Object.values(GROUPS).every(group => Array.isArray(source[group]))) {
|
|
11
|
+
throw new TypeError('Invalid Warframe codes JSON: Active, Expired and Unknown arrays are required');
|
|
12
|
+
}
|
|
13
|
+
return source;
|
|
14
|
+
}
|
|
15
|
+
function createWarframeCodes({cacheDir,githubToken,fetchImpl}={}) {
|
|
16
|
+
const storage = createDataStore({
|
|
17
|
+
gameId:'warframe-codes',
|
|
18
|
+
cacheDir:cacheDir ? path.join(path.resolve(cacheDir),'codes') : undefined,
|
|
19
|
+
bundledDir:path.join(__dirname,'..','data','codes'),
|
|
20
|
+
github:{owner:'NekoSuneProjectsNPM',repo:'game-tracker',branch:'JsonData',directory:'codes/games'},
|
|
21
|
+
syncFiles:[FILE],
|
|
22
|
+
githubToken,fetchImpl
|
|
23
|
+
});
|
|
24
|
+
async function raw() { return validate(await storage.get(FILE)); }
|
|
25
|
+
async function list({status,category,claimType,hasCode,limit}={}) {
|
|
26
|
+
if (status !== undefined && !Object.hasOwn(GROUPS,status)) throw new RangeError('status must be active, expired, or unknown');
|
|
27
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) throw new RangeError('limit must be a positive integer');
|
|
28
|
+
const doc=await raw();
|
|
29
|
+
const keys=status ? [GROUPS[status]] : Object.values(GROUPS);
|
|
30
|
+
let entries=keys.flatMap(key=>doc[key]);
|
|
31
|
+
if (category !== undefined) entries=entries.filter(item=>normalise(item.category)===normalise(category));
|
|
32
|
+
if (claimType !== undefined) entries=entries.filter(item=>normalise(item.claimType)===normalise(claimType));
|
|
33
|
+
if (hasCode !== undefined) {
|
|
34
|
+
if (typeof hasCode !== 'boolean') throw new TypeError('hasCode must be boolean');
|
|
35
|
+
entries=entries.filter(item=>Boolean(typeof item.code==='string'&&item.code.trim())===hasCode);
|
|
36
|
+
}
|
|
37
|
+
return limit ? entries.slice(0,limit) : entries;
|
|
38
|
+
}
|
|
39
|
+
async function search(query,options={}) {
|
|
40
|
+
if(typeof query!=='string'||!query.trim()) throw new TypeError('Search query is required');
|
|
41
|
+
const needle=normalise(query);
|
|
42
|
+
const entries=await list(options);
|
|
43
|
+
const results=entries.filter(record=>{
|
|
44
|
+
const fields=[record.code,record.id,record.category,record.claimType,record.notes,record.redeemLocation,
|
|
45
|
+
...(Array.isArray(record.items)?record.items.map(item=>item?.name):[])];
|
|
46
|
+
return fields.some(value=>normalise(value).includes(needle));
|
|
47
|
+
});
|
|
48
|
+
return {query,total:results.length,results,message:results.length?
|
|
49
|
+
'Found '+results.length+' Warframe code-related entries for "'+query+'".':
|
|
50
|
+
'We don\'t have anything related to "'+query+'" in Warframe codes.'};
|
|
51
|
+
}
|
|
52
|
+
async function get(key) {
|
|
53
|
+
if((typeof key!=='string' && typeof key!=='number') || !String(key).trim()) throw new TypeError('Code or ID is required');
|
|
54
|
+
const needle=normalise(key);
|
|
55
|
+
const results=(await list()).filter(item=>normalise(item.id)===needle || (item.code && normalise(item.code)===needle));
|
|
56
|
+
if(results.length===0) {
|
|
57
|
+
const err=new Error('No Warframe code entry found: '+key); err.code='NOT_FOUND';throw err;
|
|
58
|
+
}
|
|
59
|
+
if(results.length>1) {
|
|
60
|
+
const err=new Error('Multiple Warframe code entries match: '+key);err.code='AMBIGUOUS';err.results=results;throw err;
|
|
61
|
+
}
|
|
62
|
+
return results[0];
|
|
63
|
+
}
|
|
64
|
+
async function summary() {
|
|
65
|
+
const doc=await raw(); const groups={};let total=0;let redeemable=0;
|
|
66
|
+
for(const [status,key] of Object.entries(GROUPS)) {
|
|
67
|
+
const entries=doc[key];
|
|
68
|
+
const withCode=entries.filter(item=>typeof item.code==='string' && item.code.trim()).length;
|
|
69
|
+
groups[status]={total:entries.length,withCode,withoutCode:entries.length-withCode};
|
|
70
|
+
total+=entries.length;redeemable+=withCode;
|
|
71
|
+
}
|
|
72
|
+
return {total,withCode:redeemable,withoutCode:total-redeemable,groups};
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
raw,list,active:options=>list({...options,status:'active'}),
|
|
76
|
+
expired:options=>list({...options,status:'expired'}),
|
|
77
|
+
unknown:options=>list({...options,status:'unknown'}),
|
|
78
|
+
search,get,summary,sync:options=>storage.sync(options),
|
|
79
|
+
cacheDir:storage.cacheDir
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
module.exports={createWarframeCodes};
|
package/src/wfcd.js
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('node:fs/promises');
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const crypto = require('node:crypto');
|
|
6
|
-
|
|
7
|
-
// Verified WFCD GitHub repositories. All URLs are internal, never caller-provided.
|
|
8
|
-
const SOURCES = Object.freeze({
|
|
9
|
-
drops:{repo:'warframe-drop-data',branch:'main',prefix:'data/',files:['all.json','info.json','missionRewards.json','relics.json','modLocations.json','blueprintLocations.json','cetusBountyRewards.json','solarisBountyRewards.json','sortieRewards.json','transientRewards.json','deimosRewards.json','entratiLabRewards.json','hexRewards.json','zarimanRewards.json','syndicates.json']},
|
|
10
|
-
items:{repo:'warframe-items',branch:'master',prefix:'data/json/',files:['Arcanes.json','Arch-Gun.json','Arch-Melee.json','Archwing.json','Components.json','Enemy.json','Fish.json','Gear.json','Glyphs.json','Honoria.json','Melee.json','Misc.json','Mods.json','Node.json','Pets.json','Primary.json','Quests.json','Railjack.json','Relics.json','Resources.json','Secondary.json','SentinelWeapons.json','Sentinels.json','Sigils.json','Skins.json','Warframes.json']},
|
|
11
|
-
relics:{repo:'warframe-relic-data',branch:'development',prefix:'data/',files:['Relics.json','Relics.min.json','hash.json','version.json']},
|
|
12
|
-
patchlogs:{repo:'warframe-patchlogs',branch:'master',prefix:'data/',files:['patchlogs.json']}
|
|
13
|
-
});
|
|
14
|
-
const FILE_LIMIT = 16 * 1024 * 1024;
|
|
15
|
-
function createWFCD({cacheDir,githubToken,fetchImpl=globalThis.fetch}={}){
|
|
16
|
-
if(typeof fetchImpl!=='function')throw new TypeError('fetch is required');
|
|
17
|
-
const root=path.resolve(cacheDir || path.join(os.homedir(),'.cache','nekosune-game-tracker','warframe'),'wfcd');
|
|
18
|
-
const headers={Accept:'application/vnd.github+json','User-Agent':'nekosuneprojects-warframe-api',...(githubToken?{Authorization:'Bearer '+githubToken}:{})};
|
|
19
|
-
const sha=buffer=>crypto.createHash('sha1').update('blob '+buffer.length+'\0').update(buffer).digest('hex');
|
|
20
|
-
async function get(source,name,{refresh=false,signal}={}){
|
|
21
|
-
const definition=SOURCES[source];
|
|
22
|
-
if(!definition)throw new RangeError('Unknown WFCD data source');
|
|
23
|
-
if(typeof name!=='string'||!definition.files.includes(name))throw new RangeError('Unsupported '+source+' JSON file');
|
|
24
|
-
const dest=path.join(root,source,name);
|
|
25
|
-
const manifest=dest+'.sha';
|
|
26
|
-
let local;
|
|
27
|
-
try{local=await fs.readFile(dest);JSON.parse(local.toString('utf8'))}
|
|
28
|
-
catch(error){if(error.code!=='ENOENT' && !(error instanceof SyntaxError))throw error;local=null}
|
|
29
|
-
if(local && !refresh)return JSON.parse(local.toString('utf8'));
|
|
30
|
-
const remotePath=definition.prefix+name;
|
|
31
|
-
const contentsUrl='https://api.github.com/repos/WFCD/'+definition.repo+'/contents/'+remotePath+'?ref='+definition.branch;
|
|
32
|
-
try {
|
|
33
|
-
const metadataResponse=await fetchImpl(contentsUrl,{headers,signal});
|
|
34
|
-
if(!metadataResponse.ok)throw new Error('WFCD metadata HTTP '+metadataResponse.status);
|
|
35
|
-
const metadata=await metadataResponse.json();
|
|
36
|
-
if(!/^[a-f0-9]{40}$/.test(metadata.sha||'') || metadata.type!=='file' ||
|
|
37
|
-
!Number.isSafeInteger(metadata.size) || metadata.size>FILE_LIMIT)throw new Error('Invalid WFCD source metadata');
|
|
38
|
-
if(local && sha(local)===metadata.sha)return JSON.parse(local.toString('utf8'));
|
|
39
|
-
const blobURL='https://api.github.com/repos/WFCD/'+definition.repo+'/git/blobs/'+metadata.sha;
|
|
40
|
-
const response=await fetchImpl(blobURL,{headers,signal});
|
|
41
|
-
if(!response.ok)throw new Error('WFCD blob HTTP '+response.status);
|
|
42
|
-
const blob=await response.json();
|
|
43
|
-
if(blob.encoding!=='base64'||typeof blob.content!=='string')throw new Error('Unexpected WFCD blob encoding');
|
|
44
|
-
const body=Buffer.from(blob.content.replace(/\s/g,''),'base64');
|
|
45
|
-
if(body.length>FILE_LIMIT||sha(body)!==metadata.sha)throw new Error('WFCD checksum/size mismatch');
|
|
46
|
-
const value=JSON.parse(body.toString('utf8'));
|
|
47
|
-
await fs.mkdir(path.dirname(dest),{recursive:true});
|
|
48
|
-
const tmp=dest+'.'+process.pid+'.'+crypto.randomBytes(6).toString('hex')+'.tmp';
|
|
49
|
-
try{await fs.writeFile(tmp,body);await fs.rename(tmp,dest);await fs.writeFile(manifest,metadata.sha)}
|
|
50
|
-
finally{await fs.rm(tmp,{force:true})}
|
|
51
|
-
return value;
|
|
52
|
-
} catch(error) {
|
|
53
|
-
if(local && !refresh)return JSON.parse(local.toString('utf8'));
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
const groups=Object.fromEntries(Object.keys(SOURCES).map(source=>[source,Object.freeze({
|
|
58
|
-
files:Object.freeze([...SOURCES[source].files]),
|
|
59
|
-
get:(name,options)=>get(source,name,options),
|
|
60
|
-
refresh:(name,options)=>get(source,name,{...options,refresh:true}),
|
|
61
|
-
...(source==='drops'?{all:opts=>get(source,'all.json',opts),info:opts=>get(source,'info.json',opts),
|
|
62
|
-
missionRewards:opts=>get(source,'missionRewards.json',opts)}:{}),
|
|
63
|
-
...(source==='items'?{warframes:opts=>get(source,'Warframes.json',opts),
|
|
64
|
-
weapons:opts=>Promise.all(['Primary.json','Secondary.json','Melee.json'].map(name=>get(source,name,opts))).then(parts=>parts.flat()),
|
|
65
|
-
mods:opts=>get(source,'Mods.json',opts),arcanes:opts=>get(source,'Arcanes.json',opts)}:{}),
|
|
66
|
-
...(source==='relics'?{all:opts=>get(source,'Relics.json',opts)}:{}),
|
|
67
|
-
...(source==='patchlogs'?{all:opts=>get(source,'patchlogs.json',opts)}:{})
|
|
68
|
-
})]));
|
|
69
|
-
return Object.freeze({...groups,cacheDir:root});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function createMarketQuery(options={}) {
|
|
73
|
-
let pkg;
|
|
74
|
-
try {pkg=await import('warframe-nexus-query');}
|
|
75
|
-
catch(error) {
|
|
76
|
-
if(error.code==='ERR_MODULE_NOT_FOUND')throw new Error('Install optional market integration: npm install warframe-nexus-query',{cause:error});
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
const Constructor=pkg.default||pkg.PriceCheckQuerier;
|
|
80
|
-
if(typeof Constructor!=='function')throw new Error('Unsupported warframe-nexus-query export');
|
|
81
|
-
return new Constructor(options);
|
|
82
|
-
}
|
|
83
|
-
function createUpdateMonitor(options={}){
|
|
84
|
-
let Package;
|
|
85
|
-
try{Package=require('warframe-updates')}
|
|
86
|
-
catch(error){
|
|
87
|
-
if(error.code==='MODULE_NOT_FOUND')throw new Error('Install optional patch monitor: npm install warframe-updates',{cause:error});
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
if(typeof Package!=='function')throw new Error('Unsupported warframe-updates export');
|
|
91
|
-
return new Package(options);
|
|
92
|
-
}
|
|
93
|
-
module.exports={createWFCD,createMarketQuery,createUpdateMonitor,SOURCES};
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs/promises');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
|
|
7
|
+
// Verified WFCD GitHub repositories. All URLs are internal, never caller-provided.
|
|
8
|
+
const SOURCES = Object.freeze({
|
|
9
|
+
drops:{repo:'warframe-drop-data',branch:'main',prefix:'data/',files:['all.json','info.json','missionRewards.json','relics.json','modLocations.json','blueprintLocations.json','cetusBountyRewards.json','solarisBountyRewards.json','sortieRewards.json','transientRewards.json','deimosRewards.json','entratiLabRewards.json','hexRewards.json','zarimanRewards.json','syndicates.json']},
|
|
10
|
+
items:{repo:'warframe-items',branch:'master',prefix:'data/json/',files:['Arcanes.json','Arch-Gun.json','Arch-Melee.json','Archwing.json','Components.json','Enemy.json','Fish.json','Gear.json','Glyphs.json','Honoria.json','Melee.json','Misc.json','Mods.json','Node.json','Pets.json','Primary.json','Quests.json','Railjack.json','Relics.json','Resources.json','Secondary.json','SentinelWeapons.json','Sentinels.json','Sigils.json','Skins.json','Warframes.json']},
|
|
11
|
+
relics:{repo:'warframe-relic-data',branch:'development',prefix:'data/',files:['Relics.json','Relics.min.json','hash.json','version.json']},
|
|
12
|
+
patchlogs:{repo:'warframe-patchlogs',branch:'master',prefix:'data/',files:['patchlogs.json']}
|
|
13
|
+
});
|
|
14
|
+
const FILE_LIMIT = 16 * 1024 * 1024;
|
|
15
|
+
function createWFCD({cacheDir,githubToken,fetchImpl=globalThis.fetch}={}){
|
|
16
|
+
if(typeof fetchImpl!=='function')throw new TypeError('fetch is required');
|
|
17
|
+
const root=path.resolve(cacheDir || path.join(os.homedir(),'.cache','nekosune-game-tracker','warframe'),'wfcd');
|
|
18
|
+
const headers={Accept:'application/vnd.github+json','User-Agent':'nekosuneprojects-warframe-api',...(githubToken?{Authorization:'Bearer '+githubToken}:{})};
|
|
19
|
+
const sha=buffer=>crypto.createHash('sha1').update('blob '+buffer.length+'\0').update(buffer).digest('hex');
|
|
20
|
+
async function get(source,name,{refresh=false,signal}={}){
|
|
21
|
+
const definition=SOURCES[source];
|
|
22
|
+
if(!definition)throw new RangeError('Unknown WFCD data source');
|
|
23
|
+
if(typeof name!=='string'||!definition.files.includes(name))throw new RangeError('Unsupported '+source+' JSON file');
|
|
24
|
+
const dest=path.join(root,source,name);
|
|
25
|
+
const manifest=dest+'.sha';
|
|
26
|
+
let local;
|
|
27
|
+
try{local=await fs.readFile(dest);JSON.parse(local.toString('utf8'))}
|
|
28
|
+
catch(error){if(error.code!=='ENOENT' && !(error instanceof SyntaxError))throw error;local=null}
|
|
29
|
+
if(local && !refresh)return JSON.parse(local.toString('utf8'));
|
|
30
|
+
const remotePath=definition.prefix+name;
|
|
31
|
+
const contentsUrl='https://api.github.com/repos/WFCD/'+definition.repo+'/contents/'+remotePath+'?ref='+definition.branch;
|
|
32
|
+
try {
|
|
33
|
+
const metadataResponse=await fetchImpl(contentsUrl,{headers,signal});
|
|
34
|
+
if(!metadataResponse.ok)throw new Error('WFCD metadata HTTP '+metadataResponse.status);
|
|
35
|
+
const metadata=await metadataResponse.json();
|
|
36
|
+
if(!/^[a-f0-9]{40}$/.test(metadata.sha||'') || metadata.type!=='file' ||
|
|
37
|
+
!Number.isSafeInteger(metadata.size) || metadata.size>FILE_LIMIT)throw new Error('Invalid WFCD source metadata');
|
|
38
|
+
if(local && sha(local)===metadata.sha)return JSON.parse(local.toString('utf8'));
|
|
39
|
+
const blobURL='https://api.github.com/repos/WFCD/'+definition.repo+'/git/blobs/'+metadata.sha;
|
|
40
|
+
const response=await fetchImpl(blobURL,{headers,signal});
|
|
41
|
+
if(!response.ok)throw new Error('WFCD blob HTTP '+response.status);
|
|
42
|
+
const blob=await response.json();
|
|
43
|
+
if(blob.encoding!=='base64'||typeof blob.content!=='string')throw new Error('Unexpected WFCD blob encoding');
|
|
44
|
+
const body=Buffer.from(blob.content.replace(/\s/g,''),'base64');
|
|
45
|
+
if(body.length>FILE_LIMIT||sha(body)!==metadata.sha)throw new Error('WFCD checksum/size mismatch');
|
|
46
|
+
const value=JSON.parse(body.toString('utf8'));
|
|
47
|
+
await fs.mkdir(path.dirname(dest),{recursive:true});
|
|
48
|
+
const tmp=dest+'.'+process.pid+'.'+crypto.randomBytes(6).toString('hex')+'.tmp';
|
|
49
|
+
try{await fs.writeFile(tmp,body);await fs.rename(tmp,dest);await fs.writeFile(manifest,metadata.sha)}
|
|
50
|
+
finally{await fs.rm(tmp,{force:true})}
|
|
51
|
+
return value;
|
|
52
|
+
} catch(error) {
|
|
53
|
+
if(local && !refresh)return JSON.parse(local.toString('utf8'));
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const groups=Object.fromEntries(Object.keys(SOURCES).map(source=>[source,Object.freeze({
|
|
58
|
+
files:Object.freeze([...SOURCES[source].files]),
|
|
59
|
+
get:(name,options)=>get(source,name,options),
|
|
60
|
+
refresh:(name,options)=>get(source,name,{...options,refresh:true}),
|
|
61
|
+
...(source==='drops'?{all:opts=>get(source,'all.json',opts),info:opts=>get(source,'info.json',opts),
|
|
62
|
+
missionRewards:opts=>get(source,'missionRewards.json',opts)}:{}),
|
|
63
|
+
...(source==='items'?{warframes:opts=>get(source,'Warframes.json',opts),
|
|
64
|
+
weapons:opts=>Promise.all(['Primary.json','Secondary.json','Melee.json'].map(name=>get(source,name,opts))).then(parts=>parts.flat()),
|
|
65
|
+
mods:opts=>get(source,'Mods.json',opts),arcanes:opts=>get(source,'Arcanes.json',opts)}:{}),
|
|
66
|
+
...(source==='relics'?{all:opts=>get(source,'Relics.json',opts)}:{}),
|
|
67
|
+
...(source==='patchlogs'?{all:opts=>get(source,'patchlogs.json',opts)}:{})
|
|
68
|
+
})]));
|
|
69
|
+
return Object.freeze({...groups,cacheDir:root});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function createMarketQuery(options={}) {
|
|
73
|
+
let pkg;
|
|
74
|
+
try {pkg=await import('warframe-nexus-query');}
|
|
75
|
+
catch(error) {
|
|
76
|
+
if(error.code==='ERR_MODULE_NOT_FOUND')throw new Error('Install optional market integration: npm install warframe-nexus-query',{cause:error});
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
const Constructor=pkg.default||pkg.PriceCheckQuerier;
|
|
80
|
+
if(typeof Constructor!=='function')throw new Error('Unsupported warframe-nexus-query export');
|
|
81
|
+
return new Constructor(options);
|
|
82
|
+
}
|
|
83
|
+
function createUpdateMonitor(options={}){
|
|
84
|
+
let Package;
|
|
85
|
+
try{Package=require('warframe-updates')}
|
|
86
|
+
catch(error){
|
|
87
|
+
if(error.code==='MODULE_NOT_FOUND')throw new Error('Install optional patch monitor: npm install warframe-updates',{cause:error});
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if(typeof Package!=='function')throw new Error('Unsupported warframe-updates export');
|
|
91
|
+
return new Package(options);
|
|
92
|
+
}
|
|
93
|
+
module.exports={createWFCD,createMarketQuery,createUpdateMonitor,SOURCES};
|