@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.
- package/.rush/temp/package-deps_build.json +13 -12
- package/.rush/temp/shrinkwrap-deps.json +1 -1
- package/dist/common/ads/anti-cheating.d.ts +24 -51
- package/dist/common/ads/anti-cheating.test.d.ts +1 -0
- package/dist/common/ads/index.d.ts +4 -9
- package/dist/common/context/url-parse.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/index.native.js +110 -4
- package/implement.build.log +2 -2
- package/package.json +4 -3
- package/src/common/ads/anti-cheating.test.ts +79 -0
- package/src/common/ads/anti-cheating.ts +228 -139
- package/src/common/ads/index.ts +33 -20
- package/src/common/context/index.ts +4 -5
- package/src/common/context/url-parse.ts +9 -6
- package/src/native/api/ads.ts +7 -0
- package/src/native/api/navigate.ts +42 -4
- package/src/native/bootstrap/index.ts +6 -6
- package/src/native/js-core/jolibox-js-core.ts +1 -1
- package/src/native/types/native-method-map.d.ts +17 -0
- package/src/native/ui/retention.ts +152 -0
- package/src/native/bootstrap/retention.ts +0 -40
- /package/dist/native/{bootstrap → ui}/retention.d.ts +0 -0
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
interface IAdsAntiCheatingConfig {
|
|
17
|
+
export interface IAdsAntiCheatingConfig {
|
|
16
18
|
maxAllowedAdsForTime?: number; // default 8
|
|
17
|
-
|
|
18
|
-
bannedReleaseTime?: number; // default 60000 (1 minute), must be >=
|
|
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
|
-
|
|
23
|
+
banForSessionThreshold?: number; // default 600000 (10 minutes)
|
|
24
|
+
highFreqThreshold?: number; // default 2000 (2s)
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export class AdsAntiCheating {
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
-
private
|
|
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
|
-
|
|
34
|
+
readonly track: Track,
|
|
35
|
+
readonly httpClient: IHttpClient,
|
|
45
36
|
readonly checkNetwork: () => boolean,
|
|
46
37
|
config?: IAdsAntiCheatingConfig
|
|
47
38
|
) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
70
|
+
const shouldBanInitial = this.checkShouldBanInitial();
|
|
71
|
+
if (shouldBanInitial) {
|
|
72
|
+
return { reason: shouldBanInitial };
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
const shouldBanForSession = this.checkShouldBanForSession(type);
|
|
76
|
+
if (shouldBanForSession) {
|
|
77
|
+
return { reason: shouldBanForSession };
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
const shouldBanForHighFreq = this.checkShouldBanHighFreq(type);
|
|
81
|
+
if (shouldBanForHighFreq) {
|
|
82
|
+
return shouldBanForHighFreq;
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
96
|
-
*/
|
|
97
|
-
private get callAdsTimestampsForTime() {
|
|
98
|
-
return this._callAdsTimestampsForTime;
|
|
99
|
-
}
|
|
90
|
+
return { reason: 'ALLOWED' };
|
|
91
|
+
};
|
|
100
92
|
|
|
101
93
|
/**
|
|
102
|
-
*
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
if (
|
|
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
|
-
|
|
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 (
|
|
133
|
+
if (callAdsTimestampsForSession.length === 1) {
|
|
160
134
|
return undefined;
|
|
161
135
|
}
|
|
162
136
|
|
|
163
137
|
// keep last x timestamps
|
|
164
|
-
if (
|
|
165
|
-
|
|
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 (
|
|
172
|
-
const diffFromFirst = now -
|
|
173
|
-
if (diffFromFirst <=
|
|
174
|
-
|
|
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
|
-
|
|
187
|
-
if (
|
|
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
|
-
|
|
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 (
|
|
262
|
+
if (callAdsHistory.values.length === 1) {
|
|
201
263
|
return undefined;
|
|
202
264
|
}
|
|
203
265
|
|
|
204
266
|
// keep last x timestamps
|
|
205
|
-
if (
|
|
206
|
-
|
|
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 (
|
|
211
|
-
const diffFromFirst = now -
|
|
212
|
-
if (diffFromFirst <=
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
return 'NETWORK_NOT_OK';
|
|
223
|
-
}
|
|
284
|
+
const createInitialStrategy = (initialThreshold: number) => {
|
|
285
|
+
const initTimestamp = Date.now();
|
|
224
286
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
302
|
+
const createHighFreqStrategy = (highFreqThreshold = 2000) => {
|
|
303
|
+
let highFreqCounter = 0;
|
|
304
|
+
let highFreqTimer: number | null = null;
|
|
234
305
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
};
|
package/src/common/ads/index.ts
CHANGED
|
@@ -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:
|
|
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
|
|
484
|
+
const { reason, info } = this.antiCheating.checkAdsDisplayPermission(
|
|
482
485
|
params.type === 'reward' ? 'reward' : 'interstitial'
|
|
483
486
|
);
|
|
484
487
|
|
|
485
|
-
switch (
|
|
488
|
+
switch (reason) {
|
|
486
489
|
case 'NETWORK_NOT_OK':
|
|
487
490
|
case 'BANNED_FOR_SESSION':
|
|
488
|
-
console.warn('Ads not allowed',
|
|
491
|
+
// console.warn('Ads not allowed', reason);
|
|
489
492
|
params.adBreakDone?.({
|
|
490
493
|
breakType: params.type,
|
|
491
|
-
breakName:
|
|
494
|
+
breakName: reason,
|
|
492
495
|
breakFormat: params.type === 'reward' ? 'reward' : 'interstitial',
|
|
493
496
|
breakStatus: 'noAdPreloaded'
|
|
494
497
|
});
|
|
495
|
-
this.
|
|
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: '
|
|
507
|
+
breakStatus: 'frequencyCapped'
|
|
507
508
|
});
|
|
508
|
-
this.
|
|
509
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
108
|
+
return encodeJoliSourceQuery(
|
|
109
|
+
{ ...payloadJson, ...newPayloadJson },
|
|
110
|
+
urlParams.get('joliSource') ?? ''
|
|
111
|
+
);
|
|
113
112
|
}
|
|
114
113
|
return urlParams.get('joliSource') ?? '';
|
|
115
114
|
}
|