@jolibox/implement 1.1.11-beta.1 → 1.1.11-beta.10

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.
@@ -0,0 +1,79 @@
1
+ import { AdsAntiCheating, IAdsAntiCheatingConfig } from './anti-cheating';
2
+
3
+ describe('createLeadingNotifiableDebounce', () => {
4
+ jest.setTimeout(3000); // increase timeout for tes
5
+
6
+ const track = jest.fn() as any;
7
+ const http = {
8
+ get: jest.fn(),
9
+ post: jest.fn()
10
+ };
11
+
12
+ const config: IAdsAntiCheatingConfig = {
13
+ initialThreshold: 50,
14
+ highFreqThreshold: 50,
15
+ maxAllowedAdsForTime: 2,
16
+ banForTimeThreshold: 500, // 500ms
17
+ bannedReleaseTime: 500, // 500ms
18
+ banForSessionThreshold: 3000, // 4s
19
+ maxAllowedAdsForSession: 7 // 7 ads
20
+ };
21
+
22
+ it('should ban initial call', async () => {
23
+ localStorage.clear();
24
+
25
+ const adsAntiCheating = new AdsAntiCheating(track, http, () => true, config);
26
+ await new Promise((resolve) => setTimeout(resolve, 10));
27
+ const a = adsAntiCheating.checkAdsDisplayPermission('reward');
28
+ expect(a.reason).toBe('BLOCK_INITIAL');
29
+ await new Promise((resolve) => setTimeout(resolve, 100));
30
+ const b = adsAntiCheating.checkAdsDisplayPermission('reward');
31
+ expect(b.reason).toBe('ALLOWED');
32
+ });
33
+
34
+ it('should ban continous calling', async () => {
35
+ localStorage.clear();
36
+
37
+ const adsAntiCheating = new AdsAntiCheating(track, http, () => true, config);
38
+ await new Promise((resolve) => setTimeout(resolve, 100));
39
+ const a = adsAntiCheating.checkAdsDisplayPermission('reward');
40
+ expect(a.reason).toBe('ALLOWED');
41
+
42
+ // wait 10 ms
43
+ await new Promise((resolve) => setTimeout(resolve, 10));
44
+ const b = adsAntiCheating.checkAdsDisplayPermission('reward');
45
+ expect(b.reason).toBe('HOLDING_HIGH_FREQ');
46
+ expect(b.info?.count).toBe(1);
47
+
48
+ // wait another 10 ms
49
+ await new Promise((resolve) => setTimeout(resolve, 10));
50
+ const d = adsAntiCheating.checkAdsDisplayPermission('reward');
51
+ expect(d.reason).toBe('HOLDING_HIGH_FREQ');
52
+ expect(d.info?.count).toBe(2);
53
+
54
+ // wait another 100ms to reset the high freq timer, but trigger banned for time
55
+ await new Promise((resolve) => setTimeout(resolve, 60)); // wait 100ms
56
+ const c = adsAntiCheating.checkAdsDisplayPermission('reward');
57
+ expect(c.reason).toBe('BANNED_FOR_TIME');
58
+
59
+ // wait another 100ms
60
+ await new Promise((resolve) => setTimeout(resolve, 100)); // wait 100ms
61
+ const e = adsAntiCheating.checkAdsDisplayPermission('reward');
62
+ expect(e.reason).toBe('WAITING_BANNED_RELEASE');
63
+ const isBannedForTime1 = window.localStorage.getItem('jolibox-sdk-ads-ban-for-time');
64
+ expect(isBannedForTime1).toBe('true');
65
+
66
+ // wait another 400ms to reset the banned for time timer and allow display
67
+ await new Promise((resolve) => setTimeout(resolve, 400)); // wait 400ms
68
+ const f = adsAntiCheating.checkAdsDisplayPermission('reward');
69
+ expect(f.reason).toBe('ALLOWED');
70
+
71
+ // 500ms
72
+ await new Promise((resolve) => setTimeout(resolve, 500)); // wait 500ms
73
+ const g = adsAntiCheating.checkAdsDisplayPermission('reward');
74
+ expect(g.reason).toBe('BANNED_FOR_SESSION');
75
+
76
+ const isBannedForTime = window.localStorage.getItem('jolibox-sdk-ads-ban-for-time');
77
+ expect(isBannedForTime).toBe('false'); // should be false after the timer resets
78
+ });
79
+ });
@@ -1,6 +1,10 @@
1
+ import type { IHttpClient } from '../http';
2
+ import type { Track } from '../report';
3
+
1
4
  export type AdsDisplayPermission =
2
5
  | 'BLOCK_INITIAL'
3
6
  | 'BANNED_FOR_SESSION'
7
+ | 'HOLDING_HIGH_FREQ'
4
8
  | 'NETWORK_NOT_OK'
5
9
  | 'WAITING_BANNED_RELEASE'
6
10
  | 'BANNED_FOR_TIME'
@@ -10,233 +14,318 @@ interface CallAdsHistory {
10
14
  type: 'reward' | 'interstitial';
11
15
  }
12
16
 
13
- const TIMESTAMP_STORAGE_KEY = 'jolibox-sdk-ads-callbreak-timestamps';
14
-
15
- interface IAdsAntiCheatingConfig {
17
+ export interface IAdsAntiCheatingConfig {
16
18
  maxAllowedAdsForTime?: number; // default 8
17
- bannedForTimeThreshold?: number; // default 60000 (1 minute)
18
- bannedReleaseTime?: number; // default 60000 (1 minute), must be >= checkingTimeThreashold
19
+ banForTimeThreshold?: number; // default 60000 (1 minute)
20
+ bannedReleaseTime?: number; // default 60000 (1 minute), must be >= checkingTimeThreshold
19
21
  initialThreshold?: number; // default 2000
20
22
  maxAllowedAdsForSession?: number; // default 200
21
- bannedForSessionThreshold?: number; // default 600000 (10 minutes)
23
+ banForSessionThreshold?: number; // default 600000 (10 minutes)
24
+ highFreqThreshold?: number; // default 2000 (2s)
22
25
  }
23
26
 
24
27
  export class AdsAntiCheating {
25
- private maxAllowedAdsForTime = 8;
26
- private bannedForTimeThreshold = 60000;
27
- private bannedReleaseTime = 60000;
28
- private isBanningForTime = false;
29
- private releaseBannedForTimeTimeout: number | null = null;
30
-
31
- private maxAllowedAdsForSession = 200;
32
- private bannedForSessionThreshold = 600000;
33
- private isBanningForSession = false;
34
-
35
- private initialThreshold = 2000;
36
-
37
- private _callAdsTimestampsForTime: CallAdsHistory[] = []; // max x timestamps for time
38
- private callAdsTimestampsForSession: CallAdsHistory[] = []; // max x timestamps for session
39
-
40
- private initTimestamp = 0;
41
- // private context: JoliboxSDKPipeExecutor;
28
+ private checkShouldBanInitial: ReturnType<typeof createInitialStrategy>;
29
+ private checkShouldBanHighFreq: ReturnType<typeof createHighFreqStrategy>;
30
+ private checkShouldBanForTime: ReturnType<typeof createBanForTimeStrategy>;
31
+ private checkShouldBanForSession: ReturnType<typeof createBanForSessionStrategy>;
42
32
 
43
33
  constructor(
44
- // context: JoliboxSDKPipeExecutor,
34
+ readonly track: Track,
35
+ readonly httpClient: IHttpClient,
45
36
  readonly checkNetwork: () => boolean,
46
37
  config?: IAdsAntiCheatingConfig
47
38
  ) {
48
- this.maxAllowedAdsForTime = config?.maxAllowedAdsForTime ?? 8;
49
- this.bannedForTimeThreshold = config?.bannedForTimeThreshold ?? 60000;
50
- this.bannedReleaseTime = config?.bannedReleaseTime ?? 60000;
51
-
52
- this.maxAllowedAdsForSession = config?.maxAllowedAdsForSession ?? 200;
53
- this.bannedForSessionThreshold = config?.bannedForSessionThreshold ?? 600000;
54
-
55
- this.initialThreshold = config?.initialThreshold ?? 2000;
39
+ const maxAllowedAdsForTime = config?.maxAllowedAdsForTime ?? 8;
40
+ const banForTimeThreshold = config?.banForTimeThreshold ?? 60000;
41
+ const bannedReleaseTime = config?.bannedReleaseTime ?? 60000;
42
+
43
+ const maxAllowedAdsForSession = config?.maxAllowedAdsForSession ?? 200;
44
+ const banForSessionThreshold = config?.banForSessionThreshold ?? 600000;
45
+
46
+ const initialThreshold = config?.initialThreshold ?? 2000;
47
+ const highFreqThreshold = config?.highFreqThreshold ?? 2000;
48
+
49
+ this.checkShouldBanInitial = createInitialStrategy(initialThreshold);
50
+ this.checkShouldBanHighFreq = createHighFreqStrategy(highFreqThreshold);
51
+ this.checkShouldBanForTime = createBanForTimeStrategy(
52
+ maxAllowedAdsForTime,
53
+ banForTimeThreshold,
54
+ bannedReleaseTime
55
+ );
56
+ this.checkShouldBanForSession = createBanForSessionStrategy(
57
+ maxAllowedAdsForSession,
58
+ banForSessionThreshold
59
+ );
60
+ }
56
61
 
57
- if (this.maxAllowedAdsForTime <= 1) {
58
- throw new Error('maxAllowedAdsForTime must be greater than 1');
62
+ public checkAdsDisplayPermission: (type: 'reward' | 'interstitial') => {
63
+ reason: AdsDisplayPermission;
64
+ info?: { count: number };
65
+ } = (type) => {
66
+ if (!this.checkNetwork()) {
67
+ return { reason: 'NETWORK_NOT_OK' };
59
68
  }
60
69
 
61
- if (this.bannedForTimeThreshold < 0) {
62
- throw new Error('bannedForTimeThreshold must be greater than or equal to 0');
70
+ const shouldBanInitial = this.checkShouldBanInitial();
71
+ if (shouldBanInitial) {
72
+ return { reason: shouldBanInitial };
63
73
  }
64
74
 
65
- if (this.bannedReleaseTime < this.bannedForTimeThreshold) {
66
- throw new Error('bannedReleaseTime must be greater than or equal to bannedForTimeThreshold');
75
+ const shouldBanForSession = this.checkShouldBanForSession(type);
76
+ if (shouldBanForSession) {
77
+ return { reason: shouldBanForSession };
67
78
  }
68
79
 
69
- if (this.initialThreshold < 0) {
70
- throw new Error('initialThreshold must be greater than or equal to 0');
80
+ const shouldBanForHighFreq = this.checkShouldBanHighFreq(type);
81
+ if (shouldBanForHighFreq) {
82
+ return shouldBanForHighFreq;
71
83
  }
72
84
 
73
- this.initCallAdsTimestampsForTime();
74
- this.initTimestamp = Date.now();
75
- // this.context = context;
76
- }
77
-
78
- /**
79
- * Restore call ads timestamps for time from local storage
80
- */
81
- private initCallAdsTimestampsForTime = () => {
82
- try {
83
- const fromStorage = JSON.parse(window.localStorage.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
84
- if (Array.isArray(fromStorage)) {
85
- this._callAdsTimestampsForTime = fromStorage;
86
- } else {
87
- this._callAdsTimestampsForTime = [];
88
- }
89
- } catch {
90
- this._callAdsTimestampsForTime = [];
85
+ const shouldBanForTime = this.checkShouldBanForTime(type);
86
+ if (shouldBanForTime) {
87
+ return { reason: shouldBanForTime };
91
88
  }
92
- };
93
89
 
94
- /**
95
- * Get call ads timestamps for time
96
- */
97
- private get callAdsTimestampsForTime() {
98
- return this._callAdsTimestampsForTime;
99
- }
90
+ return { reason: 'ALLOWED' };
91
+ };
100
92
 
101
93
  /**
102
- * Set call ads timestamps for time, this will also save to local storage
94
+ * Report an ad display permission issue.
95
+ * Report to both event tracking system and the business backend system.
96
+ * @param reason
103
97
  */
104
- private set callAdsTimestampsForTime(timestamps: CallAdsHistory[]) {
105
- try {
106
- window.localStorage.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(timestamps));
107
- } catch {
108
- console.error('Failed to save timestamps');
109
- }
110
- this._callAdsTimestampsForTime = timestamps;
98
+ public report(reason: AdsDisplayPermission, count?: number) {
99
+ const info = count ? { reason, count } : { reason };
100
+ this.track('PreventAdsCheating', info);
101
+ this.httpClient.post('/api/base/app-event', {
102
+ data: {
103
+ eventType: 'PREVENT_ADS_CHEATING',
104
+ adsInfo: info
105
+ }
106
+ });
111
107
  }
108
+ }
112
109
 
113
- /**
114
- * Set a timeout to release the banned for time status
115
- */
116
- private setReleaseBannedForTime = () => {
117
- if (this.releaseBannedForTimeTimeout) {
118
- window.clearTimeout(this.releaseBannedForTimeTimeout);
119
- this.releaseBannedForTimeTimeout = null;
120
- }
121
- this.releaseBannedForTimeTimeout = window.setTimeout(() => {
122
- this.isBanningForTime = false;
123
- this.releaseBannedForTimeTimeout = null;
124
- }, this.bannedReleaseTime);
125
- };
126
-
127
- /**
128
- * Check if we should ban for initial call. By default, if user makes a call less than 2 seconds after the first call, block.
129
- * @returns
130
- */
131
- private checkShouldBannedInitial = () => {
132
- const now = Date.now();
133
- const diffFromInit = now - this.initTimestamp;
134
- // if initial call less than x seconds, block
135
- if (diffFromInit <= this.initialThreshold) {
136
- return 'BLOCK_INITIAL';
137
- }
138
- };
110
+ const createBanForSessionStrategy = (maxAllowedAdsForSession: number, banForSessionThreshold: number) => {
111
+ let isBannedForSession = false;
112
+ let callAdsTimestampsForSession: CallAdsHistory[] = [];
139
113
 
140
114
  /**
141
115
  * Check if we should ban this user for the whole session. By default, if user makes more than 200 calls in 10 minutes, ban for the session.
142
116
  * @param type
143
117
  * @returns
144
118
  */
145
- private checkShouldBannedForSession = (type: 'reward' | 'interstitial') => {
146
- if (this.isBanningForSession) {
119
+ const check = (type: 'reward' | 'interstitial') => {
120
+ if (isBannedForSession) {
147
121
  return 'BANNED_FOR_SESSION';
148
122
  }
149
123
 
150
124
  const now = Date.now();
151
125
 
152
126
  // keep track of call timestamps
153
- this.callAdsTimestampsForSession = this.callAdsTimestampsForSession.concat({
127
+ callAdsTimestampsForSession = callAdsTimestampsForSession.concat({
154
128
  timestamp: now,
155
129
  type
156
130
  });
157
131
 
158
132
  // if only 1 call, no need to check
159
- if (this.callAdsTimestampsForSession.length === 1) {
133
+ if (callAdsTimestampsForSession.length === 1) {
160
134
  return undefined;
161
135
  }
162
136
 
163
137
  // keep last x timestamps
164
- if (this.callAdsTimestampsForSession.length > this.maxAllowedAdsForSession) {
165
- this.callAdsTimestampsForSession = this.callAdsTimestampsForSession.slice(
166
- -this.maxAllowedAdsForSession
167
- );
138
+ if (callAdsTimestampsForSession.length > maxAllowedAdsForSession) {
139
+ callAdsTimestampsForSession = callAdsTimestampsForSession.slice(-maxAllowedAdsForSession);
168
140
  }
169
141
 
170
142
  // if less than x seconds, ban for session
171
- if (this.callAdsTimestampsForSession.length === this.maxAllowedAdsForSession) {
172
- const diffFromFirst = now - this.callAdsTimestampsForSession[0].timestamp;
173
- if (diffFromFirst <= this.bannedForSessionThreshold) {
174
- this.isBanningForSession = true;
143
+ if (callAdsTimestampsForSession.length === maxAllowedAdsForSession) {
144
+ const diffFromFirst = now - callAdsTimestampsForSession[0].timestamp;
145
+ if (diffFromFirst <= banForSessionThreshold) {
146
+ isBannedForSession = true;
175
147
  return 'BANNED_FOR_SESSION';
176
148
  }
177
149
  }
178
150
  };
179
151
 
152
+ return check;
153
+ };
154
+
155
+ const createBanForTimeStrategy = (
156
+ maxAllowedAdsForTime: number,
157
+ banForTimeThreshold: number,
158
+ bannedReleaseTime: number
159
+ ) => {
160
+ let releaseBannedForTimeTimeout: number | null = null;
161
+
162
+ const setReleaseBannedForTime = (timeout: number) => {
163
+ if (releaseBannedForTimeTimeout) {
164
+ window.clearTimeout(releaseBannedForTimeTimeout);
165
+ releaseBannedForTimeTimeout = null;
166
+ }
167
+ releaseBannedForTimeTimeout = window.setTimeout(() => {
168
+ isBannedForTime.value = false;
169
+ releaseBannedForTimeTimeout = null;
170
+ }, timeout);
171
+ };
172
+
173
+ const TIMESTAMP_STORAGE_KEY = 'jolibox-sdk-ads-callbreak-timestamps';
174
+ const BAN_FOR_TIME_KEY = 'jolibox-sdk-ads-ban-for-time';
175
+
176
+ const callAdsHistory = {
177
+ _records: [] as CallAdsHistory[],
178
+ init() {
179
+ try {
180
+ const fromStorage = JSON.parse(window.localStorage.getItem(TIMESTAMP_STORAGE_KEY) ?? '[]');
181
+ if (Array.isArray(fromStorage)) {
182
+ this._records = fromStorage;
183
+ } else {
184
+ this._records = [];
185
+ }
186
+ } catch {
187
+ this._records = [];
188
+ }
189
+ },
190
+ get values() {
191
+ return this._records;
192
+ },
193
+ set values(value: CallAdsHistory[]) {
194
+ try {
195
+ window.localStorage.setItem(TIMESTAMP_STORAGE_KEY, JSON.stringify(value));
196
+ } catch {
197
+ // console.error('Failed to save timestamps');
198
+ // do nothing
199
+ }
200
+ this._records = value;
201
+ }
202
+ };
203
+
204
+ const isBannedForTime = {
205
+ _record: false,
206
+ init() {
207
+ try {
208
+ const fromStorage = JSON.parse(window.localStorage.getItem(BAN_FOR_TIME_KEY) ?? 'false');
209
+ if (typeof fromStorage === 'boolean') {
210
+ this._record = fromStorage;
211
+ }
212
+ } catch {
213
+ this._record = false; // default value
214
+ }
215
+ if (this._record) {
216
+ const lastCall: CallAdsHistory | undefined = callAdsHistory.values[callAdsHistory.values.length - 1];
217
+ const timeDiff = lastCall ? Date.now() - lastCall.timestamp : bannedReleaseTime;
218
+ if (timeDiff <= bannedReleaseTime && timeDiff > 0) {
219
+ // within the banned release time window
220
+ setReleaseBannedForTime(timeDiff);
221
+ } else {
222
+ this._record = false;
223
+ }
224
+ }
225
+ },
226
+ get value() {
227
+ return this._record;
228
+ },
229
+ set value(value: boolean) {
230
+ try {
231
+ window.localStorage.setItem(BAN_FOR_TIME_KEY, JSON.stringify(value));
232
+ } catch {
233
+ // do nothing
234
+ }
235
+ this._record = value;
236
+ }
237
+ };
238
+
239
+ callAdsHistory.init();
240
+ isBannedForTime.init();
241
+
180
242
  /**
181
243
  * Check if we should ban this user for a period of time. By default, if user makes more than 8 calls in 1 minute, ban for 1 minute.
182
244
  * After 1 minute, user can make calls again.
183
245
  * @param type
184
246
  * @returns
185
247
  */
186
- private checkShouldBannedForTime = (type: 'reward' | 'interstitial') => {
187
- if (this.isBanningForTime) {
248
+ const check = (type: 'reward' | 'interstitial') => {
249
+ if (isBannedForTime.value) {
188
250
  return 'WAITING_BANNED_RELEASE';
189
251
  }
190
252
 
191
253
  const now = Date.now();
192
254
 
193
255
  // keep track of call timestamps
194
- this.callAdsTimestampsForTime = this.callAdsTimestampsForTime.concat({
256
+ callAdsHistory.values = callAdsHistory.values.concat({
195
257
  timestamp: now,
196
258
  type
197
259
  });
198
260
 
199
261
  // if only 1 call, no need to check
200
- if (this.callAdsTimestampsForTime.length === 1) {
262
+ if (callAdsHistory.values.length === 1) {
201
263
  return undefined;
202
264
  }
203
265
 
204
266
  // keep last x timestamps
205
- if (this.callAdsTimestampsForTime.length > this.maxAllowedAdsForTime) {
206
- this.callAdsTimestampsForTime = this.callAdsTimestampsForTime.slice(-this.maxAllowedAdsForTime);
267
+ if (callAdsHistory.values.length > maxAllowedAdsForTime) {
268
+ callAdsHistory.values = callAdsHistory.values.slice(-maxAllowedAdsForTime);
207
269
  }
208
270
 
209
271
  // if less than x seconds, ban for time
210
- if (this.callAdsTimestampsForTime.length === this.maxAllowedAdsForTime) {
211
- const diffFromFirst = now - this.callAdsTimestampsForTime[0].timestamp;
212
- if (diffFromFirst <= this.bannedForTimeThreshold) {
213
- this.isBanningForTime = true;
214
- this.setReleaseBannedForTime();
272
+ if (callAdsHistory.values.length === maxAllowedAdsForTime) {
273
+ const diffFromFirst = now - callAdsHistory.values[0].timestamp;
274
+ if (diffFromFirst <= banForTimeThreshold) {
275
+ isBannedForTime.value = true;
276
+ setReleaseBannedForTime(bannedReleaseTime);
215
277
  return 'BANNED_FOR_TIME';
216
278
  }
217
279
  }
218
280
  };
281
+ return check;
282
+ };
219
283
 
220
- public checkAdsDisplayPermission: (type: 'reward' | 'interstitial') => AdsDisplayPermission = (type) => {
221
- if (!this.checkNetwork()) {
222
- return 'NETWORK_NOT_OK';
223
- }
284
+ const createInitialStrategy = (initialThreshold: number) => {
285
+ const initTimestamp = Date.now();
224
286
 
225
- const shouldBannedInitial = this.checkShouldBannedInitial();
226
- if (shouldBannedInitial) {
227
- return shouldBannedInitial;
287
+ /**
288
+ * Check if we should ban for initial call. By default, if user makes a call less than 2 seconds after the first call, block.
289
+ * @returns
290
+ */
291
+ const check = () => {
292
+ const now = Date.now();
293
+ const diffFromInit = now - initTimestamp;
294
+ // if initial call less than x seconds, block
295
+ if (diffFromInit <= initialThreshold) {
296
+ return 'BLOCK_INITIAL';
228
297
  }
298
+ };
299
+ return check;
300
+ };
229
301
 
230
- const shouldBannedForSession = this.checkShouldBannedForSession(type);
231
- if (shouldBannedForSession) {
232
- return shouldBannedForSession;
233
- }
302
+ const createHighFreqStrategy = (highFreqThreshold = 2000) => {
303
+ let highFreqCounter = 0;
304
+ let highFreqTimer: number | null = null;
234
305
 
235
- const shouldBannedForTime = this.checkShouldBannedForTime(type);
236
- if (shouldBannedForTime) {
237
- return shouldBannedForTime;
306
+ /**
307
+ * Check if we should ban for high frequency calling. If user makes a call more than 1 times in x seconds, block.
308
+ * This only works for reward ads.
309
+ *
310
+ * @param type
311
+ * @returns
312
+ */
313
+ const check = (type: 'reward' | 'interstitial') => {
314
+ if (type === 'reward') {
315
+ if (highFreqTimer) {
316
+ highFreqCounter += 1;
317
+ return { reason: 'HOLDING_HIGH_FREQ' as const, info: { count: highFreqCounter } };
318
+ } else {
319
+ highFreqTimer = window.setTimeout(() => {
320
+ if (highFreqTimer) {
321
+ window.clearTimeout(highFreqTimer);
322
+ highFreqTimer = 0;
323
+ highFreqCounter = 0;
324
+ }
325
+ }, highFreqThreshold);
326
+ }
238
327
  }
239
-
240
- return 'ALLOWED';
241
328
  };
242
- }
329
+
330
+ return check;
331
+ };
@@ -1,9 +1,10 @@
1
1
  import { AdsActionDetection } from './ads-action-detection';
2
2
  import { AdsAntiCheating } from './anti-cheating';
3
- import { Track } from '../report';
4
3
  import { context } from '../context';
5
4
  import { ChannelPolicy } from './channel-policy';
6
5
  import { EventEmitter } from '@jolibox/common';
6
+ import type { Track } from '../report';
7
+ import type { IHttpClient } from '../http';
7
8
 
8
9
  declare global {
9
10
  interface Window {
@@ -254,10 +255,6 @@ interface IJoliboxAdsResponse {
254
255
  };
255
256
  }
256
257
 
257
- interface HttpClient {
258
- get<T>(url: string, options?: { query?: Record<string, string> }): Promise<T>;
259
- }
260
-
261
258
  export const adEventEmitter = new EventEmitter<{
262
259
  isAdShowing: [boolean];
263
260
  }>();
@@ -278,8 +275,8 @@ export class JoliboxAdsImpl {
278
275
  /**
279
276
  * Internal constructor, should not be called directly
280
277
  */
281
- constructor(readonly track: Track, readonly httpClient: HttpClient, readonly checkNetwork: () => boolean) {
282
- this.antiCheating = new AdsAntiCheating(checkNetwork);
278
+ constructor(readonly track: Track, readonly httpClient: IHttpClient, readonly checkNetwork: () => boolean) {
279
+ this.antiCheating = new AdsAntiCheating(track, httpClient, checkNetwork);
283
280
  this.adsActionDetection = new AdsActionDetection(this.track);
284
281
  this.channelPolicy = new ChannelPolicy(httpClient);
285
282
  }
@@ -400,7 +397,10 @@ export class JoliboxAdsImpl {
400
397
 
401
398
  const beforeAd = () => {
402
399
  adEventEmitter.emit('isAdShowing', true);
403
- this.track('CallBeforeAd', {});
400
+ this.track('CallBeforeAd', {
401
+ type: params.type,
402
+ name: params.name ?? ''
403
+ });
404
404
  if (originBeforeAd) {
405
405
  originBeforeAd();
406
406
  }
@@ -408,7 +408,10 @@ export class JoliboxAdsImpl {
408
408
 
409
409
  const afterAd = () => {
410
410
  adEventEmitter.emit('isAdShowing', false);
411
- this.track('CallAfterAd', {});
411
+ this.track('CallAfterAd', {
412
+ type: params.type,
413
+ name: params.name ?? ''
414
+ });
412
415
  if (originAfterAd) {
413
416
  originAfterAd();
414
417
  }
@@ -478,36 +481,46 @@ export class JoliboxAdsImpl {
478
481
  }
479
482
  }
480
483
 
481
- const adsDisplayPermission = this.antiCheating.checkAdsDisplayPermission(
484
+ const { reason, info } = this.antiCheating.checkAdsDisplayPermission(
482
485
  params.type === 'reward' ? 'reward' : 'interstitial'
483
486
  );
484
487
 
485
- switch (adsDisplayPermission) {
488
+ switch (reason) {
486
489
  case 'NETWORK_NOT_OK':
487
490
  case 'BANNED_FOR_SESSION':
488
- console.warn('Ads not allowed', adsDisplayPermission);
491
+ // console.warn('Ads not allowed', reason);
489
492
  params.adBreakDone?.({
490
493
  breakType: params.type,
491
- breakName: adsDisplayPermission,
494
+ breakName: reason,
492
495
  breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
493
496
  breakStatus: 'noAdPreloaded'
494
497
  });
495
- this.track('PreventAdsCheating', {
496
- reason: adsDisplayPermission
497
- });
498
+ this.antiCheating.report(reason);
498
499
  return;
499
500
  case 'BLOCK_INITIAL':
500
501
  case 'BANNED_FOR_TIME':
501
502
  case 'WAITING_BANNED_RELEASE':
502
- console.warn('Ads not allowed', adsDisplayPermission);
503
+ // console.warn('Ads not allowed', adsDisplayPermission);
503
504
  params.adBreakDone?.({
504
505
  breakType: params.type,
505
506
  breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
506
- breakStatus: 'viewed' // TODO: need to confirm
507
+ breakStatus: 'frequencyCapped'
507
508
  });
508
- this.track('PreventAdsCheating', {
509
- reason: adsDisplayPermission
509
+ this.antiCheating.report(reason);
510
+ return;
511
+ case 'HOLDING_HIGH_FREQ':
512
+ // follow google's style to forbid high freq request
513
+ params.adBreakDone?.({
514
+ breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
515
+ breakName: '',
516
+ breakStatus: (info?.count ?? 0) > 6 ? 'other' : 'frequencyCapped',
517
+ breakType: params.type
510
518
  });
519
+ // every 3 hit, report to anti-cheating system
520
+ // if a user click 1 time every 1 second, it will cause 20 reports for 1 minute
521
+ if (info?.count && info.count !== 0 && info.count % 3 === 0) {
522
+ this.antiCheating.report(reason, info?.count);
523
+ }
511
524
  return;
512
525
  default:
513
526
  // allowed
@@ -105,11 +105,10 @@ const wrapContext = () => {
105
105
  },
106
106
  encodeJoliSourceQuery: (newPayloadJson: QueryParams['payloadJson']) => {
107
107
  if (headerJson && signature) {
108
- return encodeJoliSourceQuery({
109
- headerJson,
110
- payloadJson: { ...payloadJson, ...newPayloadJson },
111
- signature
112
- });
108
+ return encodeJoliSourceQuery(
109
+ { ...payloadJson, ...newPayloadJson },
110
+ urlParams.get('joliSource') ?? ''
111
+ );
113
112
  }
114
113
  return urlParams.get('joliSource') ?? '';
115
114
  }