clashofclans.js 2.0.0-dev.7f4d9f8 → 2.0.0-dev.86dfaef

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 CHANGED
@@ -45,8 +45,7 @@ const client = new Client({
45
45
  });
46
46
 
47
47
  client.events.addClans(['#8P2QG08P']);
48
- client.events.setEvent({
49
- type: 'CLAN',
48
+ client.events.setClanEvent({
50
49
  name: 'clanDescriptionChange',
51
50
  filter: (oldClan, newClan) => {
52
51
  return oldClan.description !== newClan.description;
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import { ClanSearchOptions, SearchOptions, ClientOptions, InitOptions, OverrideOptions } from '../rest/RequestHandler';
3
- import { EVENTS } from '../util/Constants';
3
+ import { EVENTS, CWL_ROUNDS } from '../util/Constants';
4
4
  import { RESTManager } from '../rest/RESTManager';
5
5
  import { EventManager } from './EventManager';
6
6
  import { EventEmitter } from 'events';
@@ -40,12 +40,38 @@ export declare class Client extends EventEmitter {
40
40
  getClanMembers(clanTag: string, options?: SearchOptions): Promise<ClanMember[]>;
41
41
  /** Get clan war log. */
42
42
  getClanWarLog(clanTag: string, options?: SearchOptions): Promise<ClanWarLog[]>;
43
- /** Get info about currently running war (regular or friendly) in the clan. */
44
- getClanWar(clanTag: string, options?: OverrideOptions): Promise<ClanWar | null>;
45
- /** Get info about currently running war in the clan. */
46
- getCurrentWar(clanTag: string, options?: OverrideOptions): Promise<ClanWar | null>;
47
- /** Get info about currently running CWL round. */
48
- getLeagueWar(clanTag: string, warState?: keyof typeof CWLRound): Promise<ClanWar | null>;
43
+ /** Get info about currently running war (normal or friendly) in the clan. */
44
+ getClanWar(clanTag: string, options?: OverrideOptions): Promise<ClanWar>;
45
+ /**
46
+ * Get info about currently running war in the clan.
47
+ * @example
48
+ * ```ts
49
+ * await client.getCurrentWar('#8QU8J9LP');
50
+ * ```
51
+ * @example
52
+ * ```ts
53
+ * await client.getCurrentWar({ clanTag: '#8QU8J9LP', round: 'PREVIOUS_ROUND' });
54
+ * ```
55
+ */
56
+ getCurrentWar(clanTag: string | {
57
+ clanTag: string;
58
+ round?: keyof typeof CWL_ROUNDS;
59
+ }, options?: OverrideOptions): Promise<ClanWar | null>;
60
+ /**
61
+ * Get info about currently running CWL round.
62
+ * @example
63
+ * ```ts
64
+ * await client.getLeagueWar('#8QU8J9LP');
65
+ * ```
66
+ * @example
67
+ * ```ts
68
+ * await client.getLeagueWar({ clanTag: '#8QU8J9LP', round: 'PREVIOUS_ROUND' });
69
+ * ```
70
+ */
71
+ getLeagueWar(clanTag: string | {
72
+ clanTag: string;
73
+ round?: keyof typeof CWL_ROUNDS;
74
+ }, options?: OverrideOptions): Promise<ClanWar | null>;
49
75
  private _getCurrentLeagueWars;
50
76
  private _getClanWars;
51
77
  /** Get information about clan war league. */
@@ -54,7 +80,7 @@ export declare class Client extends EventEmitter {
54
80
  getClanWarLeagueRound(warTag: string | {
55
81
  warTag: string;
56
82
  clanTag?: string;
57
- }, options?: OverrideOptions): Promise<ClanWar | null>;
83
+ }, options?: OverrideOptions): Promise<ClanWar>;
58
84
  /** Get information about a player by tag. */
59
85
  getPlayer(playerTag: string, options?: OverrideOptions): Promise<Player>;
60
86
  /** Verify Player API token that can be found from the Game settings. */
@@ -88,8 +114,8 @@ export declare class Client extends EventEmitter {
88
114
  *
89
115
  * **Parameters**
90
116
  *
91
- * | Name | Type | Description |
92
- * | :--: | :--: | :---------: |
117
+ * | Name | Type | Description |
118
+ * | :--: | :------: | :-------------------: |
93
119
  * | `id` | `string` | Id of the new season. |
94
120
  * @public
95
121
  * @event
@@ -106,8 +132,8 @@ export declare class Client extends EventEmitter {
106
132
  *
107
133
  * **Parameters**
108
134
  *
109
- * | Name | Type | Description |
110
- * | :--: | :--: | :---------: |
135
+ * | Name | Type | Description |
136
+ * | :--------: | :------: | :------------------------------------------------: |
111
137
  * | `duration` | `number` | Duration of the maintenance break in milliseconds. |
112
138
  * @public
113
139
  * @event
@@ -135,13 +161,3 @@ export interface ClientEvents {
135
161
  [EVENTS.WAR_LOOP_END]: [];
136
162
  [EVENTS.ERROR]: [error: unknown];
137
163
  }
138
- export interface EventTypes {
139
- CLAN: [oldClan: Clan, newClan: Clan];
140
- PLAYER: [oldPlayer: Player, newPlayer: Player];
141
- CLAN_WAR: [oldWar: ClanWar, newWar: ClanWar];
142
- }
143
- export declare const CWLRound: {
144
- readonly PREVIOUS_WAR: "warEnded";
145
- readonly CURRENT_WAR: "inWar";
146
- readonly NEXT_WAR: "preparation";
147
- };
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CWLRound = exports.Client = void 0;
3
+ exports.Client = void 0;
4
4
  const Constants_1 = require("../util/Constants");
5
+ const HTTPError_1 = require("../rest/HTTPError");
5
6
  const RESTManager_1 = require("../rest/RESTManager");
6
7
  const EventManager_1 = require("./EventManager");
7
- const HTTPError_1 = require("../rest/HTTPError");
8
8
  const events_1 = require("events");
9
9
  const Util_1 = require("../util/Util");
10
10
  const struct_1 = require("../struct");
@@ -62,30 +62,52 @@ class Client extends events_1.EventEmitter {
62
62
  const { data } = await this.rest.getClanWarLog(clanTag, options);
63
63
  return data.items.map((entry) => new struct_1.ClanWarLog(this, entry));
64
64
  }
65
- /** Get info about currently running war (regular or friendly) in the clan. */
65
+ /** Get info about currently running war (normal or friendly) in the clan. */
66
66
  async getClanWar(clanTag, options) {
67
- const { data, maxAge } = await this.rest.getCurrentWar(clanTag, options);
68
- if (data.state === 'notInWar')
69
- return null;
67
+ const { data, maxAge, path, status } = await this.rest.getCurrentWar(clanTag, options);
68
+ if (data.state === 'notInWar') {
69
+ throw new HTTPError_1.HTTPError(HTTPError_1.notInWarError, status, path, maxAge);
70
+ }
70
71
  return new struct_1.ClanWar(this, data, { clanTag, maxAge });
71
72
  }
72
- /** Get info about currently running war in the clan. */
73
+ /**
74
+ * Get info about currently running war in the clan.
75
+ * @example
76
+ * ```ts
77
+ * await client.getCurrentWar('#8QU8J9LP');
78
+ * ```
79
+ * @example
80
+ * ```ts
81
+ * await client.getCurrentWar({ clanTag: '#8QU8J9LP', round: 'PREVIOUS_ROUND' });
82
+ * ```
83
+ */
73
84
  async getCurrentWar(clanTag, options) {
85
+ const args = typeof clanTag === 'string' ? { clanTag } : { clanTag: clanTag.clanTag, round: clanTag.round };
74
86
  try {
75
- const data = await this.getClanWar(clanTag, options);
76
- return data ?? (await this.getLeagueWar(clanTag));
87
+ return await this.getClanWar(args.clanTag, options);
77
88
  }
78
89
  catch (e) {
79
90
  if (e instanceof HTTPError_1.HTTPError && e.status === 403) {
80
- return this.getLeagueWar(clanTag);
91
+ return this.getLeagueWar({ clanTag: args.clanTag, round: args.round });
81
92
  }
82
93
  }
83
94
  return null;
84
95
  }
85
- /** Get info about currently running CWL round. */
86
- async getLeagueWar(clanTag, warState) {
87
- const state = (warState && exports.CWLRound[warState]) ?? 'inWar'; // eslint-disable-line
88
- const data = await this.getClanWarLeagueGroup(clanTag);
96
+ /**
97
+ * Get info about currently running CWL round.
98
+ * @example
99
+ * ```ts
100
+ * await client.getLeagueWar('#8QU8J9LP');
101
+ * ```
102
+ * @example
103
+ * ```ts
104
+ * await client.getLeagueWar({ clanTag: '#8QU8J9LP', round: 'PREVIOUS_ROUND' });
105
+ * ```
106
+ */
107
+ async getLeagueWar(clanTag, options) {
108
+ const args = typeof clanTag === 'string' ? { clanTag } : { clanTag: clanTag.clanTag, round: clanTag.round };
109
+ const state = (args.round && Constants_1.CWL_ROUNDS[args.round]) ?? 'inWar'; // eslint-disable-line
110
+ const data = await this.getClanWarLeagueGroup(args.clanTag, options);
89
111
  const rounds = data.rounds.filter((round) => !round.warTags.includes('#0'));
90
112
  if (!rounds.length)
91
113
  return null;
@@ -95,8 +117,11 @@ class Client extends events_1.EventEmitter {
95
117
  .map((round) => round.warTags)
96
118
  .flat()
97
119
  .reverse();
98
- const wars = await this.util.allSettled(warTags.map((warTag) => this.getClanWarLeagueRound({ warTag, clanTag }, { ignoreRateLimit: true })));
99
- return wars.find((war) => war?.state === state) ?? wars.at(0) ?? null;
120
+ const wars = await this.util.allSettled(warTags.map((warTag) => this.getClanWarLeagueRound({ warTag, clanTag: args.clanTag }, { ignoreRateLimit: true })));
121
+ if (args.round && args.round in Constants_1.CWL_ROUNDS) {
122
+ return wars.find((war) => war.state === state) ?? null;
123
+ }
124
+ return wars.find((war) => war.state === state) ?? wars.at(0) ?? null;
100
125
  }
101
126
  async _getCurrentLeagueWars(clanTag, options) {
102
127
  const data = await this.getClanWarLeagueGroup(clanTag, options);
@@ -106,15 +131,12 @@ class Client extends events_1.EventEmitter {
106
131
  async _getClanWars(clanTag, options) {
107
132
  const date = new Date().getDate();
108
133
  try {
109
- const data = await this.getClanWar(clanTag, options);
110
- if (!(date >= 1 && date <= 10))
111
- return data ? [data] : [];
112
- return data ? [data] : await this._getCurrentLeagueWars(clanTag);
134
+ return [await this.getClanWar(clanTag, options)];
113
135
  }
114
136
  catch (e) {
115
137
  if (!(date >= 1 && date <= 10))
116
138
  return [];
117
- if (e instanceof HTTPError_1.HTTPError && e.status === 403) {
139
+ if (e instanceof HTTPError_1.HTTPError && [200, 403].includes(e.status)) {
118
140
  return this._getCurrentLeagueWars(clanTag);
119
141
  }
120
142
  return [];
@@ -128,9 +150,10 @@ class Client extends events_1.EventEmitter {
128
150
  /** Get information about CWL round by WarTag. */
129
151
  async getClanWarLeagueRound(warTag, options) {
130
152
  const args = typeof warTag === 'string' ? { warTag } : { warTag: warTag.warTag, clanTag: warTag.clanTag };
131
- const { data, maxAge } = await this.rest.getClanWarLeagueRound(args.warTag, options);
132
- if (data.state === 'notInWar')
133
- return null;
153
+ const { data, maxAge, status, path } = await this.rest.getClanWarLeagueRound(args.warTag, options);
154
+ if (data.state === 'notInWar') {
155
+ throw new HTTPError_1.HTTPError(HTTPError_1.notInWarError, status, path, maxAge);
156
+ }
134
157
  return new struct_1.ClanWar(this, data, { warTag: args.warTag, clanTag: args.clanTag, maxAge });
135
158
  }
136
159
  /** Get information about a player by tag. */
@@ -206,8 +229,3 @@ class Client extends events_1.EventEmitter {
206
229
  }
207
230
  }
208
231
  exports.Client = Client;
209
- exports.CWLRound = {
210
- PREVIOUS_WAR: 'warEnded',
211
- CURRENT_WAR: 'inWar',
212
- NEXT_WAR: 'preparation'
213
- };
@@ -1,4 +1,5 @@
1
- import { EventTypes, Client } from './Client';
1
+ import { Clan, ClanWar, Player } from '../struct';
2
+ import { Client } from './Client';
2
3
  /** Represents Event Manager of the {@link Client} class. */
3
4
  export declare class EventManager {
4
5
  private readonly client;
@@ -13,25 +14,29 @@ export declare class EventManager {
13
14
  private _maintenanceStartTime;
14
15
  constructor(client: Client);
15
16
  /** Initialize the Event Manager to start pulling. */
16
- init(): Promise<(string | symbol)[]>;
17
+ init(): Promise<string[]>;
18
+ /** Add a clan tag to clan events. */
17
19
  addClans(...tags: string[]): this;
20
+ /** Delete a clan tag from clan events. */
18
21
  deleteClans(...tags: string[]): this;
22
+ /** Add a player tag for player events. */
19
23
  addPlayers(...tags: string[]): this;
24
+ /** Delete a player tag from player events. */
20
25
  deletePlayers(...tags: string[]): this;
26
+ /** Add a clan tag for war events. */
21
27
  addWars(...tags: string[]): this;
28
+ /** Delete a clan tag from war events. */
22
29
  deleteWars(...tags: string[]): this;
23
30
  /**
24
- * Set your own custom event.
25
- * @param event.type - `CLAN` | `PLAYER` | `CLAN_WAR`
26
- * @param event.name - Name of the event.
27
- * @param event.filter - Filter of this event. Must return a boolean value.
31
+ * Set your own custom clan event.
32
+ *
33
+ * In order to emit the custom event, you must have this filter function that returns a boolean.
28
34
  *
29
35
  * @example
30
36
  * ```js
31
- * client.events.addClans(['#2PP', '']);
37
+ * client.events.addClans(['#2PP', '#8QU8J9LP']);
32
38
  *
33
- * client.events.setEvent({
34
- * type: 'CLAN',
39
+ * client.events.setClanEvent({
35
40
  * name: 'clanMemberUpdate',
36
41
  * filter: (oldClan, newClan) => {
37
42
  * return oldClan.memberCount !== newClan.memberCount;
@@ -40,14 +45,35 @@ export declare class EventManager {
40
45
  *
41
46
  * client.on('clanMemberUpdate', (oldClan, newClan) => {
42
47
  * console.log(oldClan.memberCount, newClan.memberCount);
43
- * })
48
+ * });
49
+ *
50
+ * (async function () {
51
+ * await client.events.init();
52
+ * })();
44
53
  * ```
45
54
  * @returns
46
55
  */
47
- setEvent<K extends keyof EventTypes>(event: {
48
- type: K;
56
+ setClanEvent(event: {
57
+ name: string;
58
+ filter: (oldClan: Clan, newClan: Clan) => boolean;
59
+ }): this;
60
+ /**
61
+ * Set your own custom war event.
62
+ *
63
+ * In order to emit the custom event, you must have this filter function that returns a boolean.
64
+ */
65
+ setWarEvent(event: {
66
+ name: string;
67
+ filter: (oldWar: ClanWar, newWar: ClanWar) => boolean;
68
+ }): this;
69
+ /**
70
+ * Set your own custom player event.
71
+ *
72
+ * In order to emit the custom event, you must have this filter function that returns a boolean.
73
+ */
74
+ setPlayerEvent(event: {
49
75
  name: string;
50
- filter: (...args: EventTypes[K]) => boolean;
76
+ filter: (oldPlayer: Player, newPlayer: Player) => boolean;
51
77
  }): this;
52
78
  private maintenanceHandler;
53
79
  private seasonEndHandler;
@@ -31,36 +31,42 @@ class EventManager {
31
31
  this.warUpdateHandler();
32
32
  return Promise.resolve(this.client.eventNames());
33
33
  }
34
+ /** Add a clan tag to clan events. */
34
35
  addClans(...tags) {
35
36
  for (const tag of tags) {
36
37
  this._clanTags.add(this.client.util.parseTag(tag));
37
38
  }
38
39
  return this;
39
40
  }
41
+ /** Delete a clan tag from clan events. */
40
42
  deleteClans(...tags) {
41
43
  for (const tag of tags) {
42
44
  this._warTags.delete(this.client.util.parseTag(tag));
43
45
  }
44
46
  return this;
45
47
  }
48
+ /** Add a player tag for player events. */
46
49
  addPlayers(...tags) {
47
50
  for (const tag of tags) {
48
51
  this._playerTags.add(this.client.util.parseTag(tag));
49
52
  }
50
53
  return this;
51
54
  }
55
+ /** Delete a player tag from player events. */
52
56
  deletePlayers(...tags) {
53
57
  for (const tag of tags) {
54
58
  this._warTags.delete(this.client.util.parseTag(tag));
55
59
  }
56
60
  return this;
57
61
  }
62
+ /** Add a clan tag for war events. */
58
63
  addWars(...tags) {
59
64
  for (const tag of tags) {
60
65
  this._warTags.add(this.client.util.parseTag(tag));
61
66
  }
62
67
  return this;
63
68
  }
69
+ /** Delete a clan tag from war events. */
64
70
  deleteWars(...tags) {
65
71
  for (const tag of tags) {
66
72
  this._warTags.delete(this.client.util.parseTag(tag));
@@ -68,17 +74,15 @@ class EventManager {
68
74
  return this;
69
75
  }
70
76
  /**
71
- * Set your own custom event.
72
- * @param event.type - `CLAN` | `PLAYER` | `CLAN_WAR`
73
- * @param event.name - Name of the event.
74
- * @param event.filter - Filter of this event. Must return a boolean value.
77
+ * Set your own custom clan event.
78
+ *
79
+ * In order to emit the custom event, you must have this filter function that returns a boolean.
75
80
  *
76
81
  * @example
77
82
  * ```js
78
- * client.events.addClans(['#2PP', '']);
83
+ * client.events.addClans(['#2PP', '#8QU8J9LP']);
79
84
  *
80
- * client.events.setEvent({
81
- * type: 'CLAN',
85
+ * client.events.setClanEvent({
82
86
  * name: 'clanMemberUpdate',
83
87
  * filter: (oldClan, newClan) => {
84
88
  * return oldClan.memberCount !== newClan.memberCount;
@@ -87,27 +91,34 @@ class EventManager {
87
91
  *
88
92
  * client.on('clanMemberUpdate', (oldClan, newClan) => {
89
93
  * console.log(oldClan.memberCount, newClan.memberCount);
90
- * })
94
+ * });
95
+ *
96
+ * (async function () {
97
+ * await client.events.init();
98
+ * })();
91
99
  * ```
92
100
  * @returns
93
101
  */
94
- setEvent(event) {
95
- switch (event.type) {
96
- case 'CLAN':
97
- // @ts-expect-error
98
- this._events.clans.push(event);
99
- break;
100
- case 'PLAYER':
101
- // @ts-expect-error
102
- this._events.players.push(event);
103
- break;
104
- case 'CLAN_WAR':
105
- // @ts-expect-error
106
- this._events.wars.push(event);
107
- break;
108
- default:
109
- break;
110
- }
102
+ setClanEvent(event) {
103
+ this._events.clans.push({ name: event.name, fn: event.filter });
104
+ return this;
105
+ }
106
+ /**
107
+ * Set your own custom war event.
108
+ *
109
+ * In order to emit the custom event, you must have this filter function that returns a boolean.
110
+ */
111
+ setWarEvent(event) {
112
+ this._events.wars.push({ name: event.name, fn: event.filter });
113
+ return this;
114
+ }
115
+ /**
116
+ * Set your own custom player event.
117
+ *
118
+ * In order to emit the custom event, you must have this filter function that returns a boolean.
119
+ */
120
+ setPlayerEvent(event) {
121
+ this._events.players.push({ name: event.name, fn: event.filter });
111
122
  return this;
112
123
  }
113
124
  async maintenanceHandler() {
@@ -1,8 +1,26 @@
1
- /** Represents an HTTP Error. */
1
+ /**
2
+ * Represents an HTTP Error.
3
+ */
2
4
  export declare class HTTPError extends Error {
5
+ /** The message of this error. */
6
+ message: string;
7
+ /** The HTTP method of this request. */
3
8
  method: string;
9
+ /** The reason of this error. */
4
10
  reason: string;
11
+ /** The HTTP status code of this request. */
5
12
  status: number;
13
+ /** The path of this request. */
6
14
  path: string;
7
- constructor(error: any, status: number, path: string, method?: string);
15
+ /** Maximum number of milliseconds the results can be cached. */
16
+ maxAge: number;
17
+ constructor(error: any, status: number, path: string, maxAge: number, method?: string);
8
18
  }
19
+ export declare const notInWarError: {
20
+ message: string;
21
+ reason: string;
22
+ };
23
+ export declare const privateWarLogError: {
24
+ message: string;
25
+ reason: string;
26
+ };
@@ -1,23 +1,44 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HTTPError = void 0;
3
+ exports.privateWarLogError = exports.notInWarError = exports.HTTPError = void 0;
4
4
  const messages = {
5
- 504: 'The user aborted a request.',
6
- 404: 'Resource was not found.'
5
+ 500: 'Unknown error happened when handling the request.',
6
+ 504: 'The user aborted this request.',
7
+ 404: 'Requested resource was not found.',
8
+ 400: 'Client provided incorrect parameters for the request.',
9
+ 503: 'Service is temporarily unavailable because of maintenance.',
10
+ 429: 'Request was throttled, because amount of requests was above the threshold defined for the used API token.',
11
+ 403: 'Access denied, either because of missing/incorrect credentials or used API token does not grant access to the requested resource.'
7
12
  };
8
13
  const reasons = {
9
- 504: 'networkTimeout',
10
- 404: 'notFound'
14
+ 503: 'serviceUnavailable',
15
+ 429: 'tooManyRequests',
16
+ 400: 'badRequest',
17
+ 403: 'forbidden',
18
+ 500: 'unknownError',
19
+ 404: 'notFound',
20
+ 504: 'requestAborted'
11
21
  };
12
- /** Represents an HTTP Error. */
22
+ /**
23
+ * Represents an HTTP Error.
24
+ */
13
25
  class HTTPError extends Error {
14
- constructor(error, status, path, method = 'GET') {
26
+ constructor(error, status, path, maxAge, method) {
15
27
  super();
16
28
  this.message = error?.message ?? messages[status];
17
29
  this.reason = error?.reason ?? reasons[status];
18
30
  this.path = path;
19
- this.method = method;
31
+ this.method = method ?? 'GET';
20
32
  this.status = status;
33
+ this.maxAge = maxAge;
21
34
  }
22
35
  }
23
36
  exports.HTTPError = HTTPError;
37
+ exports.notInWarError = {
38
+ message: 'Clan is not in war at this moment.',
39
+ reason: 'notInWar'
40
+ };
41
+ exports.privateWarLogError = {
42
+ message: 'Access denied, clan war log is private.',
43
+ reason: 'privateWarLog'
44
+ };
@@ -8,120 +8,144 @@ export declare class RESTManager {
8
8
  data: APIClanList;
9
9
  maxAge: number;
10
10
  status: number;
11
+ path: string;
11
12
  }>;
12
13
  getClan(clanTag: string, options?: OverrideOptions): Promise<{
13
14
  data: APIClan;
14
15
  maxAge: number;
15
16
  status: number;
17
+ path: string;
16
18
  }>;
17
19
  getClanMembers(clanTag: string, options?: SearchOptions): Promise<{
18
20
  data: APIClanMemberList;
19
21
  maxAge: number;
20
22
  status: number;
23
+ path: string;
21
24
  }>;
22
25
  getClanWarLog(clanTag: string, options?: SearchOptions): Promise<{
23
26
  data: APIClanWarLog;
24
27
  maxAge: number;
25
28
  status: number;
29
+ path: string;
26
30
  }>;
27
31
  getCurrentWar(clanTag: string, options?: OverrideOptions): Promise<{
28
32
  data: APIClanWar;
29
33
  maxAge: number;
30
34
  status: number;
35
+ path: string;
31
36
  }>;
32
37
  getClanWarLeagueGroup(clanTag: string, options?: OverrideOptions): Promise<{
33
38
  data: APIClanWarLeagueGroup;
34
39
  maxAge: number;
35
40
  status: number;
41
+ path: string;
36
42
  }>;
37
43
  getClanWarLeagueRound(warTag: string, options?: OverrideOptions): Promise<{
38
44
  data: APIClanWar;
39
45
  maxAge: number;
40
46
  status: number;
47
+ path: string;
41
48
  }>;
42
49
  getPlayer(playerTag: string, options?: OverrideOptions): Promise<{
43
50
  data: APIPlayer;
44
51
  maxAge: number;
45
52
  status: number;
53
+ path: string;
46
54
  }>;
47
55
  postPlayerToken(playerTag: string, token: string, options?: OverrideOptions): Promise<{
48
56
  data: APIVerifyToken;
49
57
  maxAge: number;
50
58
  status: number;
59
+ path: string;
51
60
  }>;
52
61
  getLeagues(options?: SearchOptions): Promise<{
53
62
  data: APILeagueList;
54
63
  maxAge: number;
55
64
  status: number;
65
+ path: string;
56
66
  }>;
57
67
  getLeague(leagueId: string | number, options?: OverrideOptions): Promise<{
58
68
  data: APILeague;
59
69
  maxAge: number;
60
70
  status: number;
71
+ path: string;
61
72
  }>;
62
73
  getLeagueSeasons(leagueId: number, options?: SearchOptions): Promise<{
63
74
  data: APILeagueSeasonList;
64
75
  maxAge: number;
65
76
  status: number;
77
+ path: string;
66
78
  }>;
67
79
  getSeasonRankings(leagueId: number, seasonId: string, options?: SearchOptions): Promise<{
68
80
  data: APIPlayerSeasonRankingList;
69
81
  maxAge: number;
70
82
  status: number;
83
+ path: string;
71
84
  }>;
72
85
  getWarLeagues(options?: SearchOptions): Promise<{
73
86
  data: APIWarLeagueList;
74
87
  maxAge: number;
75
88
  status: number;
89
+ path: string;
76
90
  }>;
77
91
  getWarLeague(leagueId: number, options?: OverrideOptions): Promise<{
78
92
  data: APIWarLeague;
79
93
  maxAge: number;
80
94
  status: number;
95
+ path: string;
81
96
  }>;
82
97
  getLocations(options?: SearchOptions): Promise<{
83
98
  data: APILocationList;
84
99
  maxAge: number;
85
100
  status: number;
101
+ path: string;
86
102
  }>;
87
103
  getLocation(locationId: number, options?: OverrideOptions): Promise<{
88
104
  data: APILocation;
89
105
  maxAge: number;
90
106
  status: number;
107
+ path: string;
91
108
  }>;
92
109
  getClanRanks(locationId: number | string, options?: SearchOptions): Promise<{
93
110
  data: APIClanRankingList;
94
111
  maxAge: number;
95
112
  status: number;
113
+ path: string;
96
114
  }>;
97
115
  getPlayerRanks(locationId: number | string, options?: SearchOptions): Promise<{
98
116
  data: APIPlayerRankingList;
99
117
  maxAge: number;
100
118
  status: number;
119
+ path: string;
101
120
  }>;
102
121
  getVersusClanRanks(locationId: number | string, options?: SearchOptions): Promise<{
103
122
  data: APIClanVersusRankingList;
104
123
  maxAge: number;
105
124
  status: number;
125
+ path: string;
106
126
  }>;
107
127
  getVersusPlayerRanks(locationId: number | string, options?: SearchOptions): Promise<{
108
128
  data: APIPlayerVersusRankingList;
109
129
  maxAge: number;
110
130
  status: number;
131
+ path: string;
111
132
  }>;
112
133
  getClanLabels(options?: SearchOptions): Promise<{
113
134
  data: APILabelList;
114
135
  maxAge: number;
115
136
  status: number;
137
+ path: string;
116
138
  }>;
117
139
  getPlayerLabels(options?: SearchOptions): Promise<{
118
140
  data: APILabelList;
119
141
  maxAge: number;
120
142
  status: number;
143
+ path: string;
121
144
  }>;
122
145
  getGoldPassSeason(options?: OverrideOptions): Promise<{
123
146
  data: APIGoldPassSeason;
124
147
  maxAge: number;
125
148
  status: number;
149
+ path: string;
126
150
  }>;
127
151
  }
@@ -1,4 +1,5 @@
1
1
  import { QueueThrottler, BatchThrottler } from './Throttler';
2
+ import Keyv from 'keyv';
2
3
  /** Represents a Request Handler. */
3
4
  export declare class RequestHandler {
4
5
  #private;
@@ -6,7 +7,7 @@ export declare class RequestHandler {
6
7
  private password;
7
8
  private keyCount;
8
9
  private keyName;
9
- private keyDescription;
10
+ private keyDescription?;
10
11
  private keys;
11
12
  private readonly baseURL;
12
13
  private readonly retryLimit;
@@ -21,54 +22,134 @@ export declare class RequestHandler {
21
22
  data: T;
22
23
  maxAge: number;
23
24
  status: number;
25
+ path: string;
24
26
  }>;
25
27
  private exec;
26
28
  init(options: InitOptions): Promise<string[]>;
29
+ private reValidateKeys;
27
30
  private login;
28
31
  private getKeys;
29
32
  private revokeKey;
30
33
  private createKey;
31
34
  private getIp;
32
35
  }
36
+ /** Options for a client. */
33
37
  export interface ClientOptions {
38
+ /** Keys from Clash of Clans API developer site. */
34
39
  keys?: string[];
35
- cache?: boolean;
40
+ /** Base URL of the Clash of Clans API. */
36
41
  baseURL?: string;
42
+ /**
43
+ * How many times to retry on 5XX errors.
44
+ */
37
45
  retryLimit?: number;
46
+ /**
47
+ * Whether enable or disable internal caching.
48
+ * @example
49
+ * ```ts
50
+ * const client = new Client({ cache: true });
51
+ * ```
52
+ */
53
+ cache?: boolean | Keyv;
54
+ /** Time to wait before cancelling a REST request, in milliseconds. */
38
55
  restRequestTimeout?: number;
56
+ /**
57
+ * Throttler class which handles rate-limit
58
+ * @example
59
+ * ```ts
60
+ * const client = new Client({ throttler: new QueueThrottler(1000 / 10) });
61
+ * ```
62
+ * @example
63
+ * ```ts
64
+ * const client = new Client({ throttler: new BatchThrottler(30) });
65
+ * ```
66
+ */
39
67
  throttler?: QueueThrottler | BatchThrottler;
40
68
  }
69
+ /** Search options for request. */
41
70
  export interface SearchOptions extends OverrideOptions {
71
+ /** Limit the number of items returned in the response. */
42
72
  limit?: number;
73
+ /**
74
+ * Return only items that occur after this marker.
75
+ * Before marker can be found from the response, inside the 'paging' property.
76
+ * Note that only after or before can be specified for a request, not both.
77
+ */
43
78
  after?: string;
79
+ /**
80
+ * Return only items that occur before this marker.
81
+ * Before marker can be found from the response, inside the 'paging' property.
82
+ * Note that only after or before can be specified for a request, not both.
83
+ */
44
84
  before?: string;
45
85
  }
86
+ /** Override options for a request. */
46
87
  export interface OverrideOptions {
88
+ /** Whether to cache this response. */
89
+ cache?: boolean;
90
+ /** Whether to skip the cache check and request the API. */
91
+ force?: boolean;
92
+ /** How many times to retry on 5XX errors. */
47
93
  retryLimit?: string;
94
+ /** Whether to ignore throttlers. */
48
95
  ignoreRateLimit?: boolean;
96
+ /** Time to wait before cancelling a REST request, in milliseconds. */
49
97
  restRequestTimeout?: number;
50
98
  }
51
99
  export interface RequestOptions extends OverrideOptions {
52
100
  body?: string;
53
101
  method?: string;
54
102
  }
103
+ /**
104
+ * Clan search options for a request.
105
+ *
106
+ * ::info
107
+ * If name is used as part of search query, it needs to be at least three characters long.
108
+ * Name search parameter is interpreted as wild card search, so it may appear anywhere in the clan name.
109
+ * :::
110
+ */
55
111
  export interface ClanSearchOptions {
112
+ /** Search clans by name. */
56
113
  name?: string;
114
+ /** Filter by minimum number of clan members. */
57
115
  minMembers?: number;
116
+ /** Filter by maximum number of clan members. */
58
117
  maxMembers?: number;
118
+ /** Filter by minimum amount of clan points. */
59
119
  minClanPoints?: number;
120
+ /** Filter by minimum clan level. */
60
121
  minClanLevel?: number;
122
+ /** Filter by clan war frequency. */
61
123
  warFrequency?: string;
124
+ /** Filter by clan location identifier. For list of available locations, refer to getLocations operation. */
62
125
  locationId?: string;
126
+ /** Comma separated list of label IDs to use for filtering results. */
63
127
  labelIds?: string;
128
+ /** Limit the number of items returned in the response. */
64
129
  limit?: number;
130
+ /**
131
+ * Return only items that occur after this marker.
132
+ * Before marker can be found from the response, inside the 'paging' property.
133
+ * Note that only after or before can be specified for a request, not both.
134
+ */
65
135
  after?: string;
136
+ /**
137
+ * Return only items that occur before this marker.
138
+ * Before marker can be found from the response, inside the 'paging' property.
139
+ * Note that only after or before can be specified for a request, not both.
140
+ */
66
141
  before?: string;
67
142
  }
143
+ /** Login options for a client. */
68
144
  export interface InitOptions {
145
+ /** Developer site email address. */
69
146
  email: string;
147
+ /** Developer site password. */
70
148
  password: string;
149
+ /** Name of API key(s). */
71
150
  keyName?: string;
151
+ /** Number of allowed API keys. */
72
152
  keyCount?: number;
153
+ /** Description of API key(s). */
73
154
  keyDescription?: string;
74
155
  }
@@ -30,8 +30,11 @@ class RequestHandler {
30
30
  this.retryLimit = options?.retryLimit ?? 0;
31
31
  this.throttler = options?.throttler ?? null;
32
32
  this.baseURL = options?.baseURL ?? Constants_1.API_BASE_URL;
33
- this.cached = options?.cache ? new keyv_1.default() : null;
34
33
  this.restRequestTimeout = options?.restRequestTimeout ?? 0;
34
+ if (options?.cache instanceof keyv_1.default)
35
+ this.cached = options.cache;
36
+ else
37
+ this.cached = options?.cache ? new keyv_1.default() : null;
35
38
  }
36
39
  get _keys() {
37
40
  return Array.isArray(this.keys) ? this.keys : [this.keys];
@@ -47,8 +50,8 @@ class RequestHandler {
47
50
  }
48
51
  async request(path, options = {}) {
49
52
  const cached = (await this.cached?.get(path)) ?? null;
50
- if (cached)
51
- return { data: cached, maxAge: 0, status: 200 };
53
+ if (cached && options.force !== true)
54
+ return { data: cached, maxAge: 0, status: 200, path };
52
55
  if (!this.throttler || options.ignoreRateLimit)
53
56
  return this.exec(path, options);
54
57
  await this.throttler.wait();
@@ -74,23 +77,40 @@ class RequestHandler {
74
77
  await this.login();
75
78
  return this.exec(path, options, ++retries);
76
79
  }
80
+ const maxAge = Number(res?.headers.get('cache-control')?.split('=')?.[1] ?? 0) * 1000;
81
+ if (res?.status === 403 && !data?.message)
82
+ throw new HTTPError_1.HTTPError(HTTPError_1.privateWarLogError, res.status, path, maxAge);
77
83
  if (!res?.ok)
78
- throw new HTTPError_1.HTTPError(data, res?.status ?? 504, path, options.method);
79
- const maxAge = Number(res.headers.get('cache-control')?.split('=')?.[1] ?? 0) * 1000;
80
- if (this.cached && maxAge > 0)
84
+ throw new HTTPError_1.HTTPError(data, res?.status ?? 504, path, maxAge, options.method);
85
+ if (this.cached && maxAge > 0 && options.cache !== false)
81
86
  await this.cached.set(path, data, maxAge);
82
- return { data, maxAge, status: res.status };
87
+ return { data, maxAge, status: res.status, path };
83
88
  }
84
- init(options) {
89
+ async init(options) {
85
90
  if (!(options.email && options.password))
86
91
  throw ReferenceError('Missing email and password.');
87
- this.keyDescription = options.keyDescription ?? new Date().toUTCString();
92
+ this.keyDescription = options.keyDescription;
88
93
  this.keyName = options.keyName ?? 'clashofclans.js.keys';
89
94
  this.keyCount = Math.min(options.keyCount ?? 1, 10);
90
95
  this.password = options.password;
91
96
  this.email = options.email;
97
+ await this.reValidateKeys();
92
98
  return this.login();
93
99
  }
100
+ async reValidateKeys() {
101
+ for (const key of this.keys) {
102
+ const res = await (0, node_fetch_1.default)(`${this.baseURL}/locations?limit=1`, {
103
+ method: 'GET',
104
+ timeout: 10000,
105
+ headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' }
106
+ }).catch(() => null);
107
+ if (res?.status === 403) {
108
+ const index = this.keys.indexOf(key);
109
+ this.keys.splice(index, 1);
110
+ console.warn(`[WARN] Pre-defined key #${index + 1} is no longer valid. Removed from the key list.`);
111
+ }
112
+ }
113
+ }
94
114
  async login() {
95
115
  const res = await (0, node_fetch_1.default)(`${Constants_1.DEV_SITE_API_BASE_URL}/login`, {
96
116
  method: 'POST',
@@ -120,9 +140,12 @@ class RequestHandler {
120
140
  keys.splice(index, 1);
121
141
  }
122
142
  // Filter keys for current IP address and specified key name.
123
- const matching = keys.filter((key) => key.name === this.keyName && key.cidrRanges.includes(ip));
124
- if (matching.length)
125
- this.keys.push(...matching.map((key) => key.key).slice(0, this.keyCount));
143
+ for (const key of keys.filter((key) => key.name === this.keyName && key.cidrRanges.includes(ip))) {
144
+ if (this.keys.length >= this.keyCount)
145
+ break;
146
+ if (!this.keys.includes(key.key))
147
+ this.keys.push(key.key);
148
+ }
126
149
  // Create keys within limits (maximum of 10 keys per account)
127
150
  while (this.keys.length < this.keyCount && keys.length < 10) {
128
151
  const key = await this.createKey(cookie, ip);
@@ -152,7 +175,11 @@ class RequestHandler {
152
175
  const res = await (0, node_fetch_1.default)(`${Constants_1.DEV_SITE_API_BASE_URL}/apikey/create`, {
153
176
  method: 'POST',
154
177
  headers: { 'Content-Type': 'application/json', cookie },
155
- body: JSON.stringify({ cidrRanges: [ip], name: this.keyName, description: this.keyDescription })
178
+ body: JSON.stringify({
179
+ cidrRanges: [ip],
180
+ name: this.keyName,
181
+ description: this.keyDescription ?? new Date().toUTCString()
182
+ })
156
183
  });
157
184
  const data = await res.json();
158
185
  return data.key;
@@ -116,12 +116,12 @@ export declare class ClanWar {
116
116
  opponent: WarClan;
117
117
  /** The war's unique tag. This is `null` unless this is a CWL. */
118
118
  warTag: string | null;
119
- /** The timestamp when a fresh version of this data will be available again. */
119
+ /** Maximum number of milliseconds the results can be cached. */
120
120
  maxAge: number;
121
121
  constructor(client: Client, data: APIClanWar, extra: {
122
122
  clanTag?: string;
123
123
  warTag?: string;
124
- maxAge?: number;
124
+ maxAge: number;
125
125
  });
126
126
  /** Return a {@link ClanWarMember} with the tag provided. */
127
127
  getMember(tag: string): ClanWarMember | null;
@@ -129,7 +129,9 @@ export declare class ClanWar {
129
129
  getAttack(attackerTag: string, defenderTag: string): ClanWarAttack | null;
130
130
  /** Return a list of {@link ClanWarAttack} for the defenderTag provided. */
131
131
  getDefenses(defenderTag: string): ClanWarAttack[];
132
- /** Returns either `friendly`, `cwl` or `regular`. */
133
- get type(): 'friendly' | 'cwl' | 'regular';
132
+ /** Returns either `friendly`, `cwl` or `normal`. */
133
+ get type(): "friendly" | "cwl" | "normal";
134
134
  private get _isFriendly();
135
+ /** Returns the war status, based off the home clan. */
136
+ get status(): "win" | "lose" | "tie" | "pending";
135
137
  }
@@ -147,7 +147,7 @@ class ClanWar {
147
147
  }
148
148
  this.clan = new WarClan(this, clan);
149
149
  this.opponent = new WarClan(this, opponent);
150
- this.maxAge = Date.now() + (extra.maxAge ?? 0);
150
+ this.maxAge = extra.maxAge;
151
151
  }
152
152
  /** Return a {@link ClanWarMember} with the tag provided. */
153
153
  getMember(tag) {
@@ -168,17 +168,31 @@ class ClanWar {
168
168
  }
169
169
  return this.opponent.attacks.filter((atk) => atk.defenderTag === defenderTag);
170
170
  }
171
- /** Returns either `friendly`, `cwl` or `regular`. */
171
+ /** Returns either `friendly`, `cwl` or `normal`. */
172
172
  get type() {
173
173
  if (this._isFriendly)
174
174
  return 'friendly';
175
175
  if (this.warTag)
176
176
  return 'cwl';
177
- return 'regular';
177
+ return 'normal';
178
178
  }
179
179
  get _isFriendly() {
180
180
  const preparationTime = this.startTime.getTime() - this.preparationStartTime.getTime();
181
181
  return Constants_1.FRIENDLY_WAR_PREPARATION_TIMES.includes(preparationTime);
182
182
  }
183
+ /** Returns the war status, based off the home clan. */
184
+ get status() {
185
+ if (this.state === 'preparation')
186
+ return 'pending';
187
+ if (this.clan.stars > this.opponent.stars)
188
+ return 'win';
189
+ if (this.clan.stars === this.opponent.stars) {
190
+ if (this.clan.destruction > this.opponent.destruction)
191
+ return 'win';
192
+ if (this.clan.destruction === this.opponent.destruction)
193
+ return 'tie';
194
+ }
195
+ return 'lose';
196
+ }
183
197
  }
184
198
  exports.ClanWar = ClanWar;
@@ -57,7 +57,7 @@ class ClanWarLeagueGroup {
57
57
  const warTags = rounds.map((round) => round.warTags).flat();
58
58
  const wars = await Promise.allSettled(warTags.map((warTag) => this.client.getClanWarLeagueRound({ warTag, clanTag }, { ignoreRateLimit: true })));
59
59
  return wars
60
- .filter((res) => res.status === 'fulfilled' && res.value)
60
+ .filter((res) => res.status === 'fulfilled')
61
61
  .map((res) => res.value)
62
62
  .filter((war) => (clanTag ? war.clan.tag === clanTag : true));
63
63
  }
@@ -71,7 +71,7 @@ class ClanWarLeagueGroup {
71
71
  .flat();
72
72
  const wars = await Promise.allSettled(warTags.map((warTag) => this.client.getClanWarLeagueRound({ warTag, clanTag }, { ignoreRateLimit: true })));
73
73
  return wars
74
- .filter((res) => res.status === 'fulfilled' && res.value)
74
+ .filter((res) => res.status === 'fulfilled')
75
75
  .map((res) => res.value)
76
76
  .filter((war) => war.clan.tag === clanTag);
77
77
  }
@@ -49,6 +49,6 @@ export declare class ClanWarLog {
49
49
  /** The opposition clan. */
50
50
  opponent: WarLogClan;
51
51
  constructor(client: Client, data: APIClanWarLogEntry);
52
- /** Returns either `friendly`, `cwl` or `regular`. */
53
- get type(): "friendly" | "cwl" | "regular";
52
+ /** Returns either `friendly`, `cwl` or `normal`. */
53
+ get type(): "friendly" | "cwl" | "normal";
54
54
  }
@@ -34,13 +34,13 @@ class ClanWarLog {
34
34
  this.clan = new WarLogClan(data.clan);
35
35
  this.opponent = new WarLogClan(data.opponent);
36
36
  }
37
- /** Returns either `friendly`, `cwl` or `regular`. */
37
+ /** Returns either `friendly`, `cwl` or `normal`. */
38
38
  get type() {
39
39
  if (!this.clan.expEarned)
40
40
  return 'friendly';
41
41
  if (!this.opponent.tag)
42
42
  return 'cwl';
43
- return 'regular';
43
+ return 'normal';
44
44
  }
45
45
  }
46
46
  exports.ClanWarLog = ClanWarLog;
@@ -230,7 +230,7 @@ export interface APILocationList {
230
230
  items: APILocation[];
231
231
  paging: APIPaging;
232
232
  }
233
- /** /locations/{loacationId} */
233
+ /** /locations/{locationId} */
234
234
  export interface APILocation {
235
235
  localizedName?: string;
236
236
  id: number;
@@ -34,3 +34,8 @@ export declare const EVENTS: {
34
34
  readonly MAINTENANCE_END: "maintenanceEnd";
35
35
  readonly ERROR: "error";
36
36
  };
37
+ export declare const CWL_ROUNDS: {
38
+ readonly PREVIOUS_ROUND: "warEnded";
39
+ readonly CURRENT_ROUND: "inWar";
40
+ readonly NEXT_ROUND: "preparation";
41
+ };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EVENTS = exports.FRIENDLY_WAR_PREPARATION_TIMES = exports.WAR_LEAGUES = exports.LEAGUES = exports.LEGEND_LEAGUE_ID = exports.UNRANKED_LEAGUE_DATA = exports.HERO_PETS = exports.HEROES = exports.SPELLS = exports.DARK_ELIXIR_SPELLS = exports.ELIXIR_SPELLS = exports.SUPER_TROOPS = exports.SIEGE_MACHINES = exports.HOME_TROOPS = exports.DARK_ELIXIR_TROOPS = exports.ELIXIR_TROOPS = exports.DEV_SITE_API_BASE_URL = exports.API_BASE_URL = void 0;
3
+ exports.CWL_ROUNDS = exports.EVENTS = exports.FRIENDLY_WAR_PREPARATION_TIMES = exports.WAR_LEAGUES = exports.LEAGUES = exports.LEGEND_LEAGUE_ID = exports.UNRANKED_LEAGUE_DATA = exports.HERO_PETS = exports.HEROES = exports.SPELLS = exports.DARK_ELIXIR_SPELLS = exports.ELIXIR_SPELLS = exports.SUPER_TROOPS = exports.SIEGE_MACHINES = exports.HOME_TROOPS = exports.DARK_ELIXIR_TROOPS = exports.ELIXIR_TROOPS = exports.DEV_SITE_API_BASE_URL = exports.API_BASE_URL = void 0;
4
4
  exports.API_BASE_URL = 'https://api.clashofclans.com/v1';
5
5
  exports.DEV_SITE_API_BASE_URL = 'https://developer.clashofclans.com/api';
6
6
  exports.ELIXIR_TROOPS = [
@@ -115,3 +115,8 @@ exports.EVENTS = {
115
115
  MAINTENANCE_END: 'maintenanceEnd',
116
116
  ERROR: 'error'
117
117
  };
118
+ exports.CWL_ROUNDS = {
119
+ PREVIOUS_ROUND: 'warEnded',
120
+ CURRENT_ROUND: 'inWar',
121
+ NEXT_ROUND: 'preparation'
122
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clashofclans.js",
3
- "version": "2.0.0-dev.7f4d9f8",
3
+ "version": "2.0.0-dev.86dfaef",
4
4
  "description": "JavaScript library for interacting with the Clash of Clans API",
5
5
  "author": "SUVAJIT <suvajit.me@gmail.com>",
6
6
  "license": "MIT",