@fsvreddit/bot-bouncer-evaluation 0.7.40
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 +1 -0
- package/dist/allEvaluators.d.ts +16 -0
- package/dist/allEvaluators.js +72 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +6 -0
- package/dist/extendedDevvit.d.ts +36 -0
- package/dist/extendedDevvit.js +66 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +23 -0
- package/dist/userEvaluation/EvaluateBadDisplayname.d.ts +18 -0
- package/dist/userEvaluation/EvaluateBadDisplayname.js +81 -0
- package/dist/userEvaluation/EvaluateBadDisplaynameDefinedHandles.d.ts +16 -0
- package/dist/userEvaluation/EvaluateBadDisplaynameDefinedHandles.js +89 -0
- package/dist/userEvaluation/EvaluateBadUsername.d.ts +16 -0
- package/dist/userEvaluation/EvaluateBadUsername.js +75 -0
- package/dist/userEvaluation/EvaluateBioText.d.ts +18 -0
- package/dist/userEvaluation/EvaluateBioText.js +96 -0
- package/dist/userEvaluation/EvaluateBioTextDefinedHandles.d.ts +16 -0
- package/dist/userEvaluation/EvaluateBioTextDefinedHandles.js +99 -0
- package/dist/userEvaluation/EvaluateBotGroupAdvanced.d.ts +124 -0
- package/dist/userEvaluation/EvaluateBotGroupAdvanced.js +1360 -0
- package/dist/userEvaluation/EvaluateBotGroupAdvancedInternal.d.ts +6 -0
- package/dist/userEvaluation/EvaluateBotGroupAdvancedInternal.js +13 -0
- package/dist/userEvaluation/EvaluateBotGroupSubmitters.d.ts +8 -0
- package/dist/userEvaluation/EvaluateBotGroupSubmitters.js +23 -0
- package/dist/userEvaluation/EvaluateCommentPhrase.d.ts +16 -0
- package/dist/userEvaluation/EvaluateCommentPhrase.js +86 -0
- package/dist/userEvaluation/EvaluateDomainSharer.d.ts +18 -0
- package/dist/userEvaluation/EvaluateDomainSharer.js +138 -0
- package/dist/userEvaluation/EvaluateFirstCommentEmDash.d.ts +17 -0
- package/dist/userEvaluation/EvaluateFirstCommentEmDash.js +103 -0
- package/dist/userEvaluation/EvaluateFirstPostWithSelfComment.d.ts +14 -0
- package/dist/userEvaluation/EvaluateFirstPostWithSelfComment.js +53 -0
- package/dist/userEvaluation/EvaluateInconsistentAgeBot.d.ts +17 -0
- package/dist/userEvaluation/EvaluateInconsistentAgeBot.js +91 -0
- package/dist/userEvaluation/EvaluateInconsistentGenderBot.d.ts +17 -0
- package/dist/userEvaluation/EvaluateInconsistentGenderBot.js +99 -0
- package/dist/userEvaluation/EvaluateLinkReuse.d.ts +17 -0
- package/dist/userEvaluation/EvaluateLinkReuse.js +159 -0
- package/dist/userEvaluation/EvaluateMixedBot.d.ts +15 -0
- package/dist/userEvaluation/EvaluateMixedBot.js +82 -0
- package/dist/userEvaluation/EvaluateObfuscatedBioKeywords.d.ts +16 -0
- package/dist/userEvaluation/EvaluateObfuscatedBioKeywords.js +79 -0
- package/dist/userEvaluation/EvaluatePinnedPostTitle.d.ts +15 -0
- package/dist/userEvaluation/EvaluatePinnedPostTitle.js +77 -0
- package/dist/userEvaluation/EvaluatePostTitle.d.ts +16 -0
- package/dist/userEvaluation/EvaluatePostTitle.js +91 -0
- package/dist/userEvaluation/EvaluatePostTitleDefinedHandles.d.ts +15 -0
- package/dist/userEvaluation/EvaluatePostTitleDefinedHandles.js +109 -0
- package/dist/userEvaluation/EvaluatePostTitleMulti.d.ts +16 -0
- package/dist/userEvaluation/EvaluatePostTitleMulti.js +88 -0
- package/dist/userEvaluation/EvaluateRapidFireBot.d.ts +13 -0
- package/dist/userEvaluation/EvaluateRapidFireBot.js +48 -0
- package/dist/userEvaluation/EvaluateRepostBot.d.ts +19 -0
- package/dist/userEvaluation/EvaluateRepostBot.js +167 -0
- package/dist/userEvaluation/EvaluateSelfComment.d.ts +16 -0
- package/dist/userEvaluation/EvaluateSelfComment.js +74 -0
- package/dist/userEvaluation/EvaluateSocialLinks.d.ts +16 -0
- package/dist/userEvaluation/EvaluateSocialLinks.js +101 -0
- package/dist/userEvaluation/EvaluateSuspiciousFirstPost.d.ts +15 -0
- package/dist/userEvaluation/EvaluateSuspiciousFirstPost.js +68 -0
- package/dist/userEvaluation/EvaluateTGGroup.d.ts +16 -0
- package/dist/userEvaluation/EvaluateTGGroup.js +73 -0
- package/dist/userEvaluation/EvaluateTextInNsfwImages.d.ts +14 -0
- package/dist/userEvaluation/EvaluateTextInNsfwImages.js +169 -0
- package/dist/userEvaluation/EvaluateTitleCopyBot.d.ts +13 -0
- package/dist/userEvaluation/EvaluateTitleCopyBot.js +74 -0
- package/dist/userEvaluation/EvaluateWarmupBot.d.ts +15 -0
- package/dist/userEvaluation/EvaluateWarmupBot.js +89 -0
- package/dist/userEvaluation/EvaluateWorldTraveller.d.ts +18 -0
- package/dist/userEvaluation/EvaluateWorldTraveller.js +79 -0
- package/dist/userEvaluation/UserEvaluatorBase.d.ts +67 -0
- package/dist/userEvaluation/UserEvaluatorBase.js +109 -0
- package/dist/userEvaluation/evaluatorHelpers.d.ts +3 -0
- package/dist/userEvaluation/evaluatorHelpers.js +25 -0
- package/dist/userEvaluation/index.d.ts +26 -0
- package/dist/userEvaluation/index.js +42 -0
- package/dist/utility.d.ts +34 -0
- package/dist/utility.js +164 -0
- package/eslint.config.mjs +3 -0
- package/package.json +50 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,1360 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EvaluateBotGroupAdvanced = void 0;
|
|
4
|
+
const tid_js_1 = require("@devvit/public-api/types/tid.js");
|
|
5
|
+
const UserEvaluatorBase_js_1 = require("./UserEvaluatorBase.js");
|
|
6
|
+
const date_fns_1 = require("date-fns");
|
|
7
|
+
const evaluatorHelpers_js_1 = require("./evaluatorHelpers.js");
|
|
8
|
+
const lodash_1 = require("lodash");
|
|
9
|
+
const constants_js_1 = require("../constants.js");
|
|
10
|
+
const utility_js_1 = require("../utility.js");
|
|
11
|
+
function validateAgeCriteria(age) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
14
|
+
if (Object.keys(age).includes("dateFrom")) {
|
|
15
|
+
const { dateFrom, dateTo } = age;
|
|
16
|
+
if (!dateRegex.test(dateFrom)) {
|
|
17
|
+
errors.push({ severity: "error", message: "Invalid or missing dateFrom in age criteria." });
|
|
18
|
+
}
|
|
19
|
+
if (dateTo && !dateRegex.test(dateTo)) {
|
|
20
|
+
errors.push({ severity: "error", message: "Invalid date format for dateTo in age criteria. Expected format is YYYY-MM-DD." });
|
|
21
|
+
}
|
|
22
|
+
if (Object.keys(age).includes("maxAgeInDays") || Object.keys(age).includes("minAgeInDays")) {
|
|
23
|
+
errors.push({ severity: "error", message: "Cannot specify both date range and min/maxAgeInDays in age criteria." });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (Object.keys(age).includes("maxAgeInDays")) {
|
|
27
|
+
const { maxAgeInDays } = age;
|
|
28
|
+
if (typeof maxAgeInDays !== "number" || maxAgeInDays <= 0) {
|
|
29
|
+
errors.push({ severity: "error", message: "maxAgeInDays must be a positive number." });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(age).includes("minAgeInDays")) {
|
|
33
|
+
const { minAgeInDays } = age;
|
|
34
|
+
if (typeof minAgeInDays !== "number" || minAgeInDays <= 0) {
|
|
35
|
+
errors.push({ severity: "error", message: "minAgeInDays must be a positive number." });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (!Object.keys(age).includes("dateFrom") && !Object.keys(age).includes("maxAgeInDays") && !Object.keys(age).includes("minAgeInDays")) {
|
|
39
|
+
// If neither date range nor maxAgeInDays is specified, it's an error
|
|
40
|
+
errors.push({ severity: "error", message: "Age criteria must specify either date range, maxAgeInDays, or minAgeInDays." });
|
|
41
|
+
}
|
|
42
|
+
if (Object.keys(age).includes("maxAgeInMinutes")) {
|
|
43
|
+
const { maxAgeInMinutes, maxAgeInDays } = age;
|
|
44
|
+
if (typeof maxAgeInMinutes !== "number" || maxAgeInMinutes <= 0) {
|
|
45
|
+
errors.push({ severity: "error", message: "maxAgeInMinutes must be a positive number." });
|
|
46
|
+
}
|
|
47
|
+
if (maxAgeInDays !== 1) {
|
|
48
|
+
errors.push({ severity: "error", message: "maxAgeInMinutes cannot be specified without maxAgeInDays being set to 1." });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (Object.keys(age).includes("minAgeInMinutes")) {
|
|
52
|
+
const { minAgeInMinutes, minAgeInDays } = age;
|
|
53
|
+
if (typeof minAgeInMinutes !== "number" || minAgeInMinutes <= 0) {
|
|
54
|
+
errors.push({ severity: "error", message: "minAgeInMinutes must be a positive number." });
|
|
55
|
+
}
|
|
56
|
+
if (!minAgeInDays) {
|
|
57
|
+
errors.push({ severity: "error", message: "minAgeInMinutes cannot be specified without minAgeInDays being set." });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const keys = Object.keys(age);
|
|
61
|
+
const expectedKeys = ["dateFrom", "dateTo", "maxAgeInDays", "minAgeInDays", "maxAgeInMinutes", "minAgeInMinutes"];
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
if (!expectedKeys.includes(key)) {
|
|
64
|
+
errors.push({ severity: "error", message: `Unexpected key in age criteria: ${key}` });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!keys.includes("dateFrom") && !keys.includes("maxAgeInDays") && !keys.includes("minAgeInDays")) {
|
|
68
|
+
errors.push({ severity: "error", message: "Age criteria must specify at least one of dateFrom, maxAgeInDays, or minAgeInDays." });
|
|
69
|
+
}
|
|
70
|
+
return errors;
|
|
71
|
+
}
|
|
72
|
+
function validateRegexArray(regexes) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
if (!Array.isArray(regexes)) {
|
|
75
|
+
errors.push({ severity: "error", message: "Expected an array of regex strings." });
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
for (const regexString of regexes) {
|
|
79
|
+
if (Array.isArray(regexString)) {
|
|
80
|
+
errors.push({ severity: "error", message: "Regex must be a string, not an array." });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof regexString !== "string") {
|
|
84
|
+
errors.push({ severity: "error", message: `Invalid regex: ${regexString}. Must be a string.` });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const regex = new RegExp(regexString, "u");
|
|
89
|
+
if (regex.test("")) {
|
|
90
|
+
errors.push({ severity: "error", message: `Regex ${regexString} appears to be too greedy.` });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
errors.push({ severity: "error", message: `Invalid regex: ${regexString}` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return errors;
|
|
99
|
+
}
|
|
100
|
+
function validatePostCondition(condition) {
|
|
101
|
+
const errors = [];
|
|
102
|
+
if (condition.pinned !== undefined && typeof condition.pinned !== "boolean") {
|
|
103
|
+
errors.push({ severity: "error", message: "pinned must be a boolean." });
|
|
104
|
+
}
|
|
105
|
+
if (condition.titleRegex) {
|
|
106
|
+
errors.push(...validateRegexArray(condition.titleRegex));
|
|
107
|
+
}
|
|
108
|
+
if (condition.nsfw !== undefined && typeof condition.nsfw !== "boolean") {
|
|
109
|
+
errors.push({ severity: "error", message: "nsfw must be a boolean." });
|
|
110
|
+
}
|
|
111
|
+
if (condition.urlRegex) {
|
|
112
|
+
errors.push(...validateRegexArray(condition.urlRegex));
|
|
113
|
+
}
|
|
114
|
+
if (condition.domain) {
|
|
115
|
+
if (!Array.isArray(condition.domain)) {
|
|
116
|
+
errors.push({ severity: "error", message: "domain must be an array." });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (condition.domain.some(subreddit => Array.isArray(subreddit))) {
|
|
120
|
+
errors.push({ severity: "error", message: "domain must be an array of strings, not arrays." });
|
|
121
|
+
}
|
|
122
|
+
else if (condition.domain.some(name => typeof name !== "string")) {
|
|
123
|
+
errors.push({ severity: "error", message: "domain must be an array of strings." });
|
|
124
|
+
}
|
|
125
|
+
if (condition.domain.includes("")) {
|
|
126
|
+
errors.push({ severity: "error", message: "domain cannot be an empty string." });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (condition.isCrossPost !== undefined && typeof condition.isCrossPost !== "boolean") {
|
|
131
|
+
errors.push({ severity: "error", message: "isCrossPost must be a boolean." });
|
|
132
|
+
}
|
|
133
|
+
const keys = Object.keys(condition);
|
|
134
|
+
const expectedKeys = ["type", "pinned", "matchesNeeded", "distinctSubsNeeded", "age", "edited", "subredditName", "notSubredditName", "bodyRegex", "minBodyLength", "maxBodyLength", "minParaCount", "maxParaCount", "minKarma", "maxKarma", "titleRegex", "nsfw", "urlRegex", "domain", "isCrossPost"];
|
|
135
|
+
for (const key of keys) {
|
|
136
|
+
if (!expectedKeys.includes(key)) {
|
|
137
|
+
errors.push({ severity: "error", message: `Unexpected key in post condition: ${key}` });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return errors;
|
|
141
|
+
}
|
|
142
|
+
function validateCommentCondition(condition) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
if (condition.postId !== undefined) {
|
|
145
|
+
if (!Array.isArray(condition.postId)) {
|
|
146
|
+
errors.push({ severity: "error", message: "postId must be an array." });
|
|
147
|
+
}
|
|
148
|
+
else if (condition.postId.some(postId => typeof postId !== "string")) {
|
|
149
|
+
errors.push({ severity: "error", message: "postId must be an array of strings." });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const validPostIdRegex = /^[a-z0-9]{6,8}$/;
|
|
153
|
+
if (!condition.postId.every(id => validPostIdRegex.test(id))) {
|
|
154
|
+
errors.push({ severity: "error", message: `Invalid postId(s): ${condition.postId.filter(id => !validPostIdRegex.test(id)).join(", ")}. Must be a 6-8 character lower-case alphanumeric string.` });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (condition.isTopLevel !== undefined && typeof condition.isTopLevel !== "boolean") {
|
|
159
|
+
errors.push({ severity: "error", message: "isTopLevel must be a boolean." });
|
|
160
|
+
}
|
|
161
|
+
if (condition.isCommentOnOwnPost !== undefined && typeof condition.isCommentOnOwnPost !== "boolean") {
|
|
162
|
+
errors.push({ severity: "error", message: "isCommentOnOwnPost must be a boolean." });
|
|
163
|
+
}
|
|
164
|
+
if (condition.postAuthorNameRegex) {
|
|
165
|
+
errors.push(...validateRegexArray(condition.postAuthorNameRegex));
|
|
166
|
+
}
|
|
167
|
+
if (condition.postTitleRegex) {
|
|
168
|
+
errors.push(...validateRegexArray(condition.postTitleRegex));
|
|
169
|
+
}
|
|
170
|
+
if (condition.postBodyRegex) {
|
|
171
|
+
errors.push(...validateRegexArray(condition.postBodyRegex));
|
|
172
|
+
}
|
|
173
|
+
if (condition.postUrlRegex) {
|
|
174
|
+
errors.push(...validateRegexArray(condition.postUrlRegex));
|
|
175
|
+
}
|
|
176
|
+
if (condition.postCreatedAtAge) {
|
|
177
|
+
errors.push(...validateAgeCriteria(condition.postCreatedAtAge).map(error => ({ severity: error.severity, message: `Post created at age criteria: ${error.message}` })));
|
|
178
|
+
}
|
|
179
|
+
const keys = Object.keys(condition);
|
|
180
|
+
const expectedKeys = ["type", "matchesNeeded", "distinctSubsNeeded", "age", "edited", "subredditName", "notSubredditName", "bodyRegex", "minBodyLength", "maxBodyLength", "minParaCount", "maxParaCount", "minKarma", "maxKarma", "postId", "isTopLevel", "isCommentOnOwnPost", "postAuthorNameRegex", "postTitleRegex", "postBodyRegex", "postUrlRegex", "postCreatedAtAge"];
|
|
181
|
+
for (const key of keys) {
|
|
182
|
+
if (!expectedKeys.includes(key)) {
|
|
183
|
+
errors.push({ severity: "error", message: `Unexpected key in comment condition: ${key}` });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return errors;
|
|
187
|
+
}
|
|
188
|
+
function validateCondition(condition) {
|
|
189
|
+
const errors = [];
|
|
190
|
+
if (typeof condition.type !== "string" || !["post", "comment"].includes(condition.type)) {
|
|
191
|
+
errors.push({ severity: "error", message: "type must be either 'post' or 'comment'. Check indents!" });
|
|
192
|
+
}
|
|
193
|
+
if (condition.matchesNeeded !== undefined && typeof condition.matchesNeeded !== "number") {
|
|
194
|
+
errors.push({ severity: "error", message: "matchesNeeded must be a number." });
|
|
195
|
+
}
|
|
196
|
+
if (condition.distinctSubsNeeded !== undefined && typeof condition.distinctSubsNeeded !== "number") {
|
|
197
|
+
errors.push({ severity: "error", message: "distinctSubsNeeded must be a number." });
|
|
198
|
+
}
|
|
199
|
+
if (condition.matchesNeeded !== undefined && condition.matchesNeeded < 1) {
|
|
200
|
+
errors.push({ severity: "error", message: "matchesNeeded must be at least 1." });
|
|
201
|
+
}
|
|
202
|
+
if (condition.edited !== undefined && typeof condition.edited !== "boolean") {
|
|
203
|
+
errors.push({ severity: "error", message: "edited must be a boolean." });
|
|
204
|
+
}
|
|
205
|
+
if (condition.age) {
|
|
206
|
+
errors.push(...validateAgeCriteria(condition.age).map(error => ({ severity: error.severity, message: `Age criteria: ${error.message}` })));
|
|
207
|
+
}
|
|
208
|
+
if (condition.subredditName) {
|
|
209
|
+
if (!Array.isArray(condition.subredditName)) {
|
|
210
|
+
errors.push({ severity: "error", message: "subredditName must be an array." });
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
if (condition.subredditName.some(subreddit => Array.isArray(subreddit))) {
|
|
214
|
+
errors.push({ severity: "error", message: "subredditName must be an array of strings, not arrays." });
|
|
215
|
+
}
|
|
216
|
+
else if (condition.subredditName.some(name => typeof name !== "string")) {
|
|
217
|
+
errors.push({ severity: "error", message: "subredditName must be an array of strings." });
|
|
218
|
+
}
|
|
219
|
+
if (condition.subredditName.includes("")) {
|
|
220
|
+
errors.push({ severity: "error", message: "subredditName cannot be an empty string." });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (condition.notSubredditName) {
|
|
225
|
+
if (!Array.isArray(condition.notSubredditName)) {
|
|
226
|
+
errors.push({ severity: "error", message: "notSubredditName must be an array." });
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
if (condition.notSubredditName.some(subreddit => Array.isArray(subreddit))) {
|
|
230
|
+
errors.push({ severity: "error", message: "notSubredditName must be an array of strings, not arrays." });
|
|
231
|
+
}
|
|
232
|
+
else if (condition.notSubredditName.some(name => typeof name !== "string")) {
|
|
233
|
+
errors.push({ severity: "error", message: "notSubredditName must be an array of strings." });
|
|
234
|
+
}
|
|
235
|
+
if (condition.notSubredditName.includes("")) {
|
|
236
|
+
errors.push({ severity: "error", message: "notSubredditName cannot be an empty string." });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (condition.bodyRegex) {
|
|
241
|
+
errors.push(...validateRegexArray(condition.bodyRegex));
|
|
242
|
+
}
|
|
243
|
+
if (condition.minBodyLength !== undefined && (typeof condition.minBodyLength !== "number" || condition.minBodyLength < 0)) {
|
|
244
|
+
errors.push({ severity: "error", message: "minBodyLength must be a non-negative number." });
|
|
245
|
+
}
|
|
246
|
+
if (condition.maxBodyLength !== undefined && (typeof condition.maxBodyLength !== "number" || condition.maxBodyLength < 0)) {
|
|
247
|
+
errors.push({ severity: "error", message: "maxBodyLength must be a non-negative number." });
|
|
248
|
+
}
|
|
249
|
+
if (condition.minParaCount !== undefined && (typeof condition.minParaCount !== "number" || condition.minParaCount < 0)) {
|
|
250
|
+
errors.push({ severity: "error", message: "minParaCount must be a non-negative number." });
|
|
251
|
+
}
|
|
252
|
+
if (condition.maxParaCount !== undefined && (typeof condition.maxParaCount !== "number" || condition.maxParaCount < 0)) {
|
|
253
|
+
errors.push({ severity: "error", message: "maxParaCount must be a non-negative number." });
|
|
254
|
+
}
|
|
255
|
+
if (condition.minKarma !== undefined && (typeof condition.minKarma !== "number" || condition.minKarma < 0)) {
|
|
256
|
+
errors.push({ severity: "error", message: "minKarma must be a non-negative number." });
|
|
257
|
+
}
|
|
258
|
+
if (condition.maxKarma !== undefined && (typeof condition.maxKarma !== "number" || condition.maxKarma < 0)) {
|
|
259
|
+
errors.push({ severity: "error", message: "maxKarma must be a non-negative number." });
|
|
260
|
+
}
|
|
261
|
+
if (condition.type === "post") {
|
|
262
|
+
errors.push(...validatePostCondition(condition));
|
|
263
|
+
}
|
|
264
|
+
else { // Comment condition
|
|
265
|
+
errors.push(...validateCommentCondition(condition));
|
|
266
|
+
}
|
|
267
|
+
return errors;
|
|
268
|
+
}
|
|
269
|
+
function validateCriteriaGroup(criteria, level = 0) {
|
|
270
|
+
const errors = [];
|
|
271
|
+
if (Array.isArray(criteria)) {
|
|
272
|
+
errors.push({ severity: "error", message: "Criteria cannot be an array. Use 'every' or 'some' to combine conditions." });
|
|
273
|
+
return errors;
|
|
274
|
+
}
|
|
275
|
+
const subKeys = Object.keys(criteria).filter(key => ["not", "every", "some", "type"].includes(key));
|
|
276
|
+
if (subKeys.length === 0) {
|
|
277
|
+
errors.push({ severity: "error", message: "Criteria must contain one condition or a group (not, every, some)." });
|
|
278
|
+
return errors;
|
|
279
|
+
}
|
|
280
|
+
if (subKeys.length > 1) {
|
|
281
|
+
errors.push({ severity: "error", message: "Criteria cannot contain multiple top-level conditions. Use 'every' or 'some' to combine them." });
|
|
282
|
+
return errors;
|
|
283
|
+
}
|
|
284
|
+
if ("not" in criteria) {
|
|
285
|
+
if (level > 1) {
|
|
286
|
+
errors.push({ severity: "error", message: "Nested 'not' conditions are not allowed." });
|
|
287
|
+
}
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
289
|
+
if (criteria.not === null) {
|
|
290
|
+
errors.push({ severity: "error", message: "'not' condition must not be empty." });
|
|
291
|
+
}
|
|
292
|
+
else if (typeof criteria.not !== "object" || Array.isArray(criteria.not)) {
|
|
293
|
+
errors.push({ severity: "error", message: "'not' condition must be a single condition." });
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
errors.push(...validateCriteriaGroup(criteria.not, level + 1));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if ("every" in criteria) {
|
|
300
|
+
if (!Array.isArray(criteria.every)) {
|
|
301
|
+
errors.push({ severity: "error", message: "'every' must be an array of conditions." });
|
|
302
|
+
}
|
|
303
|
+
else if (criteria.every.length === 0) {
|
|
304
|
+
errors.push({ severity: "error", message: "'every' must not be an empty array." });
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
for (const subCriteria of criteria.every) {
|
|
308
|
+
errors.push(...validateCriteriaGroup(subCriteria, level + 1));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if ("some" in criteria) {
|
|
313
|
+
if (!Array.isArray(criteria.some)) {
|
|
314
|
+
errors.push({ severity: "error", message: "'some' must be an array of conditions." });
|
|
315
|
+
}
|
|
316
|
+
else if (criteria.some.length === 0) {
|
|
317
|
+
errors.push({ severity: "error", message: "'some' must not be an empty array." });
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
if (criteria.some.some(subCriteria => "not" in subCriteria)) {
|
|
321
|
+
errors.push({ severity: "error", message: "Nested 'not' conditions within 'or' are not allowed." });
|
|
322
|
+
}
|
|
323
|
+
for (const subCriteria of criteria.some) {
|
|
324
|
+
errors.push(...validateCriteriaGroup(subCriteria, level + 1));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
errors.push(...validateCondition(criteria));
|
|
330
|
+
}
|
|
331
|
+
const keys = Object.keys(criteria);
|
|
332
|
+
const expectedKeys = ["not", "every", "some", "type", "pinned", "matchesNeeded", "distinctSubsNeeded", "age", "edited", "subredditName", "notSubredditName", "bodyRegex", "titleRegex", "nsfw", "urlRegex", "domain", "postId", "isTopLevel", "isCommentOnOwnPost", "minBodyLength", "maxBodyLength", "minParaCount", "maxParaCount", "minKarma", "maxKarma", "postAuthorNameRegex", "postTitleRegex", "postBodyRegex", "postUrlRegex", "postCreatedAtAge", "isCrossPost"];
|
|
333
|
+
for (const key of keys) {
|
|
334
|
+
if (!expectedKeys.includes(key)) {
|
|
335
|
+
errors.push({ severity: "error", message: `Unexpected key in criteria group: ${key}` });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return errors;
|
|
339
|
+
}
|
|
340
|
+
function validateBotGroup(group, allowNewFeatures, evaluatorName) {
|
|
341
|
+
const errors = [];
|
|
342
|
+
if (group === null) {
|
|
343
|
+
return [{ severity: "error", message: "Bot group contains no properties." }];
|
|
344
|
+
}
|
|
345
|
+
if (!group.name) {
|
|
346
|
+
errors.push({ severity: "error", message: "Bot group name is required." });
|
|
347
|
+
}
|
|
348
|
+
if (typeof group.name !== "string") {
|
|
349
|
+
errors.push({ severity: "error", message: "Bot group name must be a string. You may need to enclose the group name in single quotes." });
|
|
350
|
+
}
|
|
351
|
+
if (group.submitterName !== undefined) {
|
|
352
|
+
if (typeof group.submitterName !== "string" && !Array.isArray(group.submitterName)) {
|
|
353
|
+
errors.push({ severity: "error", message: "submitterName must be a string or an array of strings." });
|
|
354
|
+
}
|
|
355
|
+
else if (Array.isArray(group.submitterName) && group.submitterName.some(name => typeof name !== "string")) {
|
|
356
|
+
errors.push({ severity: "error", message: "All elements of submitterName array must be strings." });
|
|
357
|
+
}
|
|
358
|
+
if (evaluatorName !== constants_js_1.SUBMITTER_BOT_NAME) {
|
|
359
|
+
errors.push({ severity: "error", message: "submitterName is not allowed in this evaluator." });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (group.descriptionForAI !== undefined && typeof group.descriptionForAI !== "string") {
|
|
363
|
+
errors.push({ severity: "error", message: "descriptionForAI must be a string." });
|
|
364
|
+
}
|
|
365
|
+
if (group.usernameRegex) {
|
|
366
|
+
errors.push(...validateRegexArray(group.usernameRegex));
|
|
367
|
+
}
|
|
368
|
+
if (group.matchesDefaultUsernameRegex !== undefined && typeof group.matchesDefaultUsernameRegex !== "boolean") {
|
|
369
|
+
errors.push({ severity: "error", message: "matchesDefaultUsernameRegex must be a boolean." });
|
|
370
|
+
}
|
|
371
|
+
if (group.matchesDefaultUsernameRegex !== undefined && !allowNewFeatures) {
|
|
372
|
+
errors.push({ severity: "error", message: "matchesDefaultUsernameRegex is not allowed in this evaluator." });
|
|
373
|
+
}
|
|
374
|
+
if (group.age) {
|
|
375
|
+
errors.push(...validateAgeCriteria(group.age).map(error => ({ severity: error.severity, message: `Account age: ${error.message}` })));
|
|
376
|
+
}
|
|
377
|
+
if (group.bioRegex) {
|
|
378
|
+
errors.push(...validateRegexArray(group.bioRegex));
|
|
379
|
+
}
|
|
380
|
+
if (group.displayNameRegex) {
|
|
381
|
+
errors.push(...validateRegexArray(group.displayNameRegex));
|
|
382
|
+
}
|
|
383
|
+
if (group.socialLinkRegex) {
|
|
384
|
+
errors.push(...validateRegexArray(group.socialLinkRegex));
|
|
385
|
+
}
|
|
386
|
+
if (group.socialLinkTitleRegex) {
|
|
387
|
+
errors.push(...validateRegexArray(group.socialLinkTitleRegex));
|
|
388
|
+
if (!group.socialLinkRegex || group.socialLinkRegex.length === 0) {
|
|
389
|
+
errors.push({ severity: "error", message: "socialLinkTitleRegex is specified but socialLinkRegex is missing. socialLinkRegex must be specified if socialLinkTitleRegex is used." });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (group.hasNoSocialLinks !== undefined && typeof group.hasNoSocialLinks !== "boolean") {
|
|
393
|
+
errors.push({ severity: "error", message: "hasNoSocialLinks must be a boolean." });
|
|
394
|
+
}
|
|
395
|
+
if (group.criteria) {
|
|
396
|
+
errors.push(...validateCriteriaGroup(group.criteria));
|
|
397
|
+
}
|
|
398
|
+
if (group.maxLinkKarma !== undefined) {
|
|
399
|
+
if (typeof group.maxLinkKarma !== "number") {
|
|
400
|
+
errors.push({ severity: "error", message: "Max link karma must be a number." });
|
|
401
|
+
}
|
|
402
|
+
else if (group.maxLinkKarma <= 0) {
|
|
403
|
+
errors.push({ severity: "error", message: "Max link karma must be a positive number." });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (group.maxCommentKarma !== undefined) {
|
|
407
|
+
if (typeof group.maxCommentKarma !== "number") {
|
|
408
|
+
errors.push({ severity: "error", message: "Max comment karma must be a number." });
|
|
409
|
+
}
|
|
410
|
+
else if (group.maxCommentKarma <= 0) {
|
|
411
|
+
errors.push({ severity: "error", message: "Max comment karma must be a positive number." });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (group.minLinkKarma !== undefined) {
|
|
415
|
+
if (typeof group.minLinkKarma !== "number") {
|
|
416
|
+
errors.push({ severity: "error", message: "Min link karma must be a number." });
|
|
417
|
+
}
|
|
418
|
+
else if (group.minLinkKarma < 0) {
|
|
419
|
+
errors.push({ severity: "error", message: "Min link karma must be a non-negative number." });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (group.minCommentKarma !== undefined) {
|
|
423
|
+
if (typeof group.minCommentKarma !== "number") {
|
|
424
|
+
errors.push({ severity: "error", message: "Min comment karma must be a number." });
|
|
425
|
+
}
|
|
426
|
+
else if (group.minCommentKarma < 0) {
|
|
427
|
+
errors.push({ severity: "error", message: "Min comment karma must be a non-negative number." });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (group.nsfw !== undefined && typeof group.nsfw !== "boolean") {
|
|
431
|
+
errors.push({ severity: "error", message: "NSFW must be a boolean." });
|
|
432
|
+
}
|
|
433
|
+
if (group.hasVerifiedEmail !== undefined && typeof group.hasVerifiedEmail !== "boolean") {
|
|
434
|
+
errors.push({ severity: "error", message: "Has verified email must be a boolean." });
|
|
435
|
+
}
|
|
436
|
+
if (group.hasRedditPremium !== undefined && typeof group.hasRedditPremium !== "boolean") {
|
|
437
|
+
errors.push({ severity: "error", message: "Has Reddit Premium must be a boolean." });
|
|
438
|
+
}
|
|
439
|
+
if (group.isSubredditModerator !== undefined && typeof group.isSubredditModerator !== "boolean") {
|
|
440
|
+
errors.push({ severity: "error", message: "Is subreddit moderator must be a boolean." });
|
|
441
|
+
}
|
|
442
|
+
if (group.hasMoreThanOneCommentOnPosts !== undefined && typeof group.hasMoreThanOneCommentOnPosts !== "boolean") {
|
|
443
|
+
errors.push({ severity: "error", message: "Has more than one comment on posts must be a boolean." });
|
|
444
|
+
}
|
|
445
|
+
const keys = Object.keys(group);
|
|
446
|
+
const expectedKeys = ["name", "submitterName", "descriptionForAI", "usernameRegex", "matchesDefaultUsernameRegex", "maxCommentKarma", "maxLinkKarma", "minCommentKarma", "minLinkKarma", "age", "nsfw", "bioRegex", "displayNameRegex", "socialLinkRegex", "socialLinkTitleRegex", "hasNoSocialLinks", "hasVerifiedEmail", "hasRedditPremium", "isSubredditModerator", "hasMoreThanOneCommentOnPosts", "criteria"];
|
|
447
|
+
for (const key of keys) {
|
|
448
|
+
if (!expectedKeys.includes(key)) {
|
|
449
|
+
errors.push({ severity: "error", message: `Unexpected key in bot group: ${key}` });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return errors;
|
|
453
|
+
}
|
|
454
|
+
class EvaluateBotGroupAdvanced extends UserEvaluatorBase_js_1.UserEvaluatorBase {
|
|
455
|
+
constructor(context, history, socialLinks, variables) {
|
|
456
|
+
super(context, history, socialLinks, variables);
|
|
457
|
+
this.name = "Bot Group Advanced";
|
|
458
|
+
this.shortname = "botgroupadvanced";
|
|
459
|
+
this.banContentThreshold = 0; // No content ban threshold for this evaluator to support account properties only checks
|
|
460
|
+
this.verboseLogging = false;
|
|
461
|
+
this.allowNewFeatures = false;
|
|
462
|
+
this.cachedPostProperties = {};
|
|
463
|
+
this.postPropertiesStatistics = {
|
|
464
|
+
totalTime: 0,
|
|
465
|
+
inMemoryHits: 0,
|
|
466
|
+
cacheHits: 0,
|
|
467
|
+
cacheMisses: 0,
|
|
468
|
+
errors: 0,
|
|
469
|
+
};
|
|
470
|
+
this.verboseLogging = this.getVariable("verboseLogging", false);
|
|
471
|
+
this.redis = context.appSlug === constants_js_1.MAIN_APP_NAME ? context.redis.global : context.redis;
|
|
472
|
+
}
|
|
473
|
+
anyRegexMatches(input, regexes) {
|
|
474
|
+
return regexes.some(regex => new RegExp(regex, "u").test(input));
|
|
475
|
+
}
|
|
476
|
+
getBotGroups() {
|
|
477
|
+
const groups = this.getAllVariables("group");
|
|
478
|
+
return Object.values(groups);
|
|
479
|
+
}
|
|
480
|
+
validateVariables() {
|
|
481
|
+
const errors = [];
|
|
482
|
+
const groups = this.getAllVariables("group");
|
|
483
|
+
for (const [key, group] of Object.entries(groups)) {
|
|
484
|
+
errors.push(...validateBotGroup(group, this.allowNewFeatures, this.shortname).map(error => ({ severity: error.severity, message: `Bot group ${key}: ${error.message}` })));
|
|
485
|
+
}
|
|
486
|
+
return errors;
|
|
487
|
+
}
|
|
488
|
+
uniqueMatchReasons(reasons) {
|
|
489
|
+
const uniqueReasons = {};
|
|
490
|
+
for (const reason of reasons) {
|
|
491
|
+
uniqueReasons[`${reason.key}:${reason.value}`] = reason;
|
|
492
|
+
}
|
|
493
|
+
return Object.values(uniqueReasons);
|
|
494
|
+
}
|
|
495
|
+
getCriteriaGroupRegexes(criteria, groupName) {
|
|
496
|
+
if ("not" in criteria) {
|
|
497
|
+
return this.getCriteriaGroupRegexes(criteria.not, groupName);
|
|
498
|
+
}
|
|
499
|
+
else if ("every" in criteria) {
|
|
500
|
+
return criteria.every.flatMap(subCriteria => this.getCriteriaGroupRegexes(subCriteria, groupName));
|
|
501
|
+
}
|
|
502
|
+
else if ("some" in criteria) {
|
|
503
|
+
return criteria.some.flatMap(subCriteria => this.getCriteriaGroupRegexes(subCriteria, groupName));
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const regexes = [];
|
|
507
|
+
if (criteria.bodyRegex) {
|
|
508
|
+
regexes.push(...criteria.bodyRegex.map(regex => ({ evaluatorName: this.name, subName: groupName, regex, flags: "u" })));
|
|
509
|
+
}
|
|
510
|
+
if (criteria.type === "post") {
|
|
511
|
+
if (criteria.titleRegex) {
|
|
512
|
+
regexes.push(...criteria.titleRegex.map(regex => ({ evaluatorName: this.name, subName: groupName, regex, flags: "u" })));
|
|
513
|
+
}
|
|
514
|
+
if (criteria.urlRegex) {
|
|
515
|
+
regexes.push(...criteria.urlRegex.map(regex => ({ evaluatorName: this.name, subName: groupName, regex })));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
if (criteria.postTitleRegex) {
|
|
520
|
+
regexes.push(...criteria.postTitleRegex.map(regex => ({ evaluatorName: this.name, subName: groupName, regex, flags: "u" })));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return regexes;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
getBotGroupRegexes(group) {
|
|
527
|
+
const regexes = [];
|
|
528
|
+
if (group.usernameRegex) {
|
|
529
|
+
for (const regex of group.usernameRegex) {
|
|
530
|
+
regexes.push({ evaluatorName: this.name, subName: group.name, regex });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (group.bioRegex) {
|
|
534
|
+
for (const regex of group.bioRegex) {
|
|
535
|
+
regexes.push({ evaluatorName: this.name, subName: group.name, regex, flags: "u" });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (group.displayNameRegex) {
|
|
539
|
+
for (const regex of group.displayNameRegex) {
|
|
540
|
+
regexes.push({ evaluatorName: this.name, subName: group.name, regex, flags: "u" });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (group.socialLinkRegex) {
|
|
544
|
+
for (const regex of group.socialLinkRegex) {
|
|
545
|
+
regexes.push({ evaluatorName: this.name, subName: group.name, regex });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (group.socialLinkTitleRegex) {
|
|
549
|
+
for (const regex of group.socialLinkTitleRegex) {
|
|
550
|
+
regexes.push({ evaluatorName: this.name, subName: group.name, regex, flags: "u" });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (group.criteria) {
|
|
554
|
+
regexes.push(...this.getCriteriaGroupRegexes(group.criteria, group.name));
|
|
555
|
+
}
|
|
556
|
+
return regexes;
|
|
557
|
+
}
|
|
558
|
+
gatherRegexes() {
|
|
559
|
+
const groups = this.getBotGroups();
|
|
560
|
+
return (0, lodash_1.uniq)(groups.flatMap(group => this.getBotGroupRegexes(group)));
|
|
561
|
+
}
|
|
562
|
+
getSubGroups() {
|
|
563
|
+
const botGroups = this.getBotGroups();
|
|
564
|
+
if (botGroups.length === 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
return botGroups.map(group => group.name);
|
|
568
|
+
}
|
|
569
|
+
matchesAgeCriteria(date, age, baseline = new Date()) {
|
|
570
|
+
// For absolute date range checks, baseline is not used.
|
|
571
|
+
if ("dateFrom" in age) {
|
|
572
|
+
const dateFrom = (0, date_fns_1.parse)(age.dateFrom, "yyyy-MM-dd", new Date());
|
|
573
|
+
if (date < dateFrom) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
if (age.dateTo) {
|
|
577
|
+
const dateTo = (0, date_fns_1.parse)(age.dateTo, "yyyy-MM-dd", new Date());
|
|
578
|
+
if (date > (0, date_fns_1.endOfDay)(dateTo)) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Baseline is only used for relative checks.
|
|
584
|
+
if ("maxAgeInDays" in age && age.maxAgeInDays) {
|
|
585
|
+
const maxAgeDate = (0, date_fns_1.subDays)(baseline, age.maxAgeInDays);
|
|
586
|
+
if (date < maxAgeDate) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if ("minAgeInDays" in age && age.minAgeInDays) {
|
|
591
|
+
const minAgeDate = (0, date_fns_1.subDays)(baseline, age.minAgeInDays);
|
|
592
|
+
if (date > minAgeDate) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if ("maxAgeInMinutes" in age && age.maxAgeInMinutes) {
|
|
597
|
+
const maxAgeDate = (0, date_fns_1.subMinutes)(baseline, age.maxAgeInMinutes);
|
|
598
|
+
if (date < maxAgeDate) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if ("minAgeInMinutes" in age && age.minAgeInMinutes) {
|
|
603
|
+
const minAgeDate = (0, date_fns_1.subMinutes)(baseline, age.minAgeInMinutes);
|
|
604
|
+
if (date > minAgeDate) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
anySubredditMatches(item, subredditNames) {
|
|
611
|
+
const subredditName = "subredditName" in item ? item.subredditName : this.context.subredditName;
|
|
612
|
+
const authorName = "authorName" in item ? item.authorName : undefined;
|
|
613
|
+
return subredditNames.some(subreddit => subredditName?.toLowerCase() === subreddit.toLowerCase())
|
|
614
|
+
|| subredditNames.some(subreddit => subreddit === "$profile" && subredditName === `u_${authorName}`);
|
|
615
|
+
}
|
|
616
|
+
postOrCommentMatchesCondition(item, condition) {
|
|
617
|
+
const matchReasons = [];
|
|
618
|
+
if (condition.age) {
|
|
619
|
+
const referenceDate = item.createdAt instanceof Date ? item.createdAt : new Date(item.createdAt);
|
|
620
|
+
if (!this.matchesAgeCriteria(referenceDate, condition.age)) {
|
|
621
|
+
return { matched: false };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (condition.edited !== undefined && "edited" in item && item.edited !== condition.edited) {
|
|
625
|
+
return { matched: false };
|
|
626
|
+
}
|
|
627
|
+
if (condition.minBodyLength) {
|
|
628
|
+
if (item.body === undefined) {
|
|
629
|
+
return { matched: false };
|
|
630
|
+
}
|
|
631
|
+
if (item.body.length < condition.minBodyLength) {
|
|
632
|
+
return { matched: false };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (condition.maxBodyLength) {
|
|
636
|
+
const body = item.body ?? "";
|
|
637
|
+
if (body.length > condition.maxBodyLength) {
|
|
638
|
+
return { matched: false };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (condition.minParaCount) {
|
|
642
|
+
if (item.body === undefined) {
|
|
643
|
+
return { matched: false };
|
|
644
|
+
}
|
|
645
|
+
const paraCount = item.body.split("\n").filter(para => para.trim() !== "").length;
|
|
646
|
+
if (paraCount < condition.minParaCount) {
|
|
647
|
+
return { matched: false };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (condition.maxParaCount !== undefined) {
|
|
651
|
+
const body = item.body ?? "";
|
|
652
|
+
const paraCount = body.split("\n").filter(para => para.trim() !== "").length;
|
|
653
|
+
if (paraCount > condition.maxParaCount) {
|
|
654
|
+
return { matched: false };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (condition.subredditName) {
|
|
658
|
+
if (this.anySubredditMatches(item, condition.subredditName)) {
|
|
659
|
+
if ("subredditName" in item) {
|
|
660
|
+
matchReasons.push({ key: "subredditName", value: item.subredditName });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
return { matched: false };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (condition.notSubredditName && this.anySubredditMatches(item, condition.notSubredditName)) {
|
|
668
|
+
return { matched: false };
|
|
669
|
+
}
|
|
670
|
+
if (condition.minKarma !== undefined) {
|
|
671
|
+
if (item.score < condition.minKarma) {
|
|
672
|
+
return { matched: false };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (condition.maxKarma !== undefined) {
|
|
676
|
+
if (item.score > condition.maxKarma) {
|
|
677
|
+
return { matched: false };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (condition.bodyRegex) {
|
|
681
|
+
if (item.body === undefined) {
|
|
682
|
+
return { matched: false }; // Body regex check requires body to be present
|
|
683
|
+
}
|
|
684
|
+
if (this.anyRegexMatches(item.body, condition.bodyRegex)) {
|
|
685
|
+
matchReasons.push({ key: "bodyRegex", value: item.body });
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
return { matched: false };
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return { matched: true, reasons: matchReasons };
|
|
692
|
+
}
|
|
693
|
+
collectCommentConditionsForPreEvalation(criteria) {
|
|
694
|
+
const conditions = [];
|
|
695
|
+
if ("type" in criteria && criteria.type === "comment") {
|
|
696
|
+
conditions.push(criteria);
|
|
697
|
+
}
|
|
698
|
+
else if ("every" in criteria) {
|
|
699
|
+
for (const subCriteria of criteria.every) {
|
|
700
|
+
conditions.push(...this.collectCommentConditionsForPreEvalation(subCriteria));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else if ("some" in criteria) {
|
|
704
|
+
for (const subCriteria of criteria.some) {
|
|
705
|
+
conditions.push(...this.collectCommentConditionsForPreEvalation(subCriteria));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return conditions;
|
|
709
|
+
}
|
|
710
|
+
collectNegatedCommentConditionsForPreEvaluation(criteria) {
|
|
711
|
+
const conditions = [];
|
|
712
|
+
if ("not" in criteria) {
|
|
713
|
+
conditions.push(...this.collectCommentConditionsForPreEvalation(criteria.not));
|
|
714
|
+
}
|
|
715
|
+
else if ("every" in criteria) {
|
|
716
|
+
for (const subCriteria of criteria.every) {
|
|
717
|
+
conditions.push(...this.collectNegatedCommentConditionsForPreEvaluation(subCriteria));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return conditions.filter(condition => condition.matchesNeeded === undefined || condition.matchesNeeded === 1);
|
|
721
|
+
}
|
|
722
|
+
async preEvaluateComment(comment) {
|
|
723
|
+
if (!comment.comment || !comment.author) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
const groups = this.getBotGroups();
|
|
727
|
+
let userInfo;
|
|
728
|
+
for (const group of groups) {
|
|
729
|
+
if (group.usernameRegex) {
|
|
730
|
+
if (!this.anyRegexMatches(comment.author.name, group.usernameRegex)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (group.bioRegex) {
|
|
735
|
+
if (!comment.author.description) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (!this.anyRegexMatches(comment.author.description, group.bioRegex)) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (group.maxCommentKarma !== undefined && comment.author.karma > group.maxCommentKarma) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (group.maxLinkKarma !== undefined && comment.author.karma > group.maxLinkKarma) {
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (!group.criteria) {
|
|
749
|
+
if (this.verboseLogging) {
|
|
750
|
+
console.log(`Pre-evaluation: Comment ${comment.comment.id} for ${comment.author.name} matches bot group with no criteria ${group.name}`);
|
|
751
|
+
}
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
;
|
|
755
|
+
const negativeCommentConditions = this.collectNegatedCommentConditionsForPreEvaluation(group.criteria);
|
|
756
|
+
let negativeConditionMatched = false;
|
|
757
|
+
for (const condition of negativeCommentConditions) {
|
|
758
|
+
const conditionMatches = await this.commentMatchesCondition(comment.comment, condition);
|
|
759
|
+
if (conditionMatches.matched) {
|
|
760
|
+
negativeConditionMatched = true;
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (negativeConditionMatched) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
const commentConditions = this.collectCommentConditionsForPreEvalation(group.criteria);
|
|
768
|
+
let commentConditionMatched = false;
|
|
769
|
+
const matchReasons = [];
|
|
770
|
+
for (const condition of commentConditions) {
|
|
771
|
+
const conditionMatches = await this.commentMatchesCondition(comment.comment, condition);
|
|
772
|
+
if (conditionMatches.matched) {
|
|
773
|
+
commentConditionMatched = true;
|
|
774
|
+
if (this.verboseLogging) {
|
|
775
|
+
matchReasons.push(...(conditionMatches.reasons ?? []));
|
|
776
|
+
}
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (!commentConditionMatched) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
if (group.age || group.displayNameRegex || group.nsfw || group.hasRedditPremium || group.hasVerifiedEmail || group.isSubredditModerator) {
|
|
784
|
+
userInfo ?? (userInfo = await (0, utility_js_1.getAdditionalUserInfo)(comment.author.name, this.context));
|
|
785
|
+
if (!userInfo) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (group.age) {
|
|
789
|
+
if (!this.matchesAgeCriteria(userInfo.createdAt, group.age)) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (group.displayNameRegex) {
|
|
794
|
+
if (!userInfo.displayName || !this.anyRegexMatches(userInfo.displayName, group.displayNameRegex)) {
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (group.nsfw !== undefined) {
|
|
799
|
+
if (userInfo.nsfw !== group.nsfw) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (group.hasRedditPremium !== undefined) {
|
|
804
|
+
if (userInfo.hasRedditPremium !== group.hasRedditPremium) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (group.hasVerifiedEmail !== undefined) {
|
|
809
|
+
if (userInfo.hasVerifiedEmail !== group.hasVerifiedEmail) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (group.isSubredditModerator !== undefined) {
|
|
814
|
+
if (userInfo.isSubredditModerator !== group.isSubredditModerator) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
console.log(`Pre-evaluation: Comment ${comment.comment.id} for ${comment.author.name} matches bot group ${group.name}`);
|
|
820
|
+
for (const reason of matchReasons) {
|
|
821
|
+
console.log(`- Matched reason: ${reason.key} = ${reason.value}`);
|
|
822
|
+
}
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
postMatchesCondition(post, condition) {
|
|
828
|
+
const conditionMatch = this.postOrCommentMatchesCondition(post, condition);
|
|
829
|
+
if (!conditionMatch.matched) {
|
|
830
|
+
return { matched: false };
|
|
831
|
+
}
|
|
832
|
+
const matchReasons = [...(conditionMatch.reasons ?? []).map(reason => ({ key: `post ${reason.key}`, value: reason.value }))];
|
|
833
|
+
if (condition.pinned !== undefined && post.stickied !== condition.pinned) {
|
|
834
|
+
return { matched: false };
|
|
835
|
+
}
|
|
836
|
+
if (condition.nsfw !== undefined && post.nsfw !== condition.nsfw) {
|
|
837
|
+
return { matched: false };
|
|
838
|
+
}
|
|
839
|
+
if (condition.titleRegex) {
|
|
840
|
+
if (this.anyRegexMatches(post.title, condition.titleRegex)) {
|
|
841
|
+
matchReasons.push({ key: "titleRegex", value: post.title });
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
return { matched: false };
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (condition.urlRegex) {
|
|
848
|
+
if (this.anyRegexMatches(post.url, condition.urlRegex)) {
|
|
849
|
+
matchReasons.push({ key: "urlRegex", value: post.url });
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
return { matched: false };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (condition.domain) {
|
|
856
|
+
const domain = (0, evaluatorHelpers_js_1.domainFromUrl)(post.url);
|
|
857
|
+
if (!domain) {
|
|
858
|
+
return { matched: false };
|
|
859
|
+
}
|
|
860
|
+
if (!condition.domain.includes(domain)) {
|
|
861
|
+
return { matched: false };
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (condition.isCrossPost !== undefined) {
|
|
865
|
+
const isCrossPost = post.crosspostParentId !== undefined;
|
|
866
|
+
if (isCrossPost !== condition.isCrossPost) {
|
|
867
|
+
return { matched: false };
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return { matched: true, reasons: matchReasons };
|
|
871
|
+
}
|
|
872
|
+
async getPostProperties(postId) {
|
|
873
|
+
const now = Date.now();
|
|
874
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
875
|
+
if (this.cachedPostProperties[postId]) {
|
|
876
|
+
this.postPropertiesStatistics.inMemoryHits++;
|
|
877
|
+
return this.cachedPostProperties[postId];
|
|
878
|
+
}
|
|
879
|
+
const cacheKey = `bbe~postProperties~${postId}`;
|
|
880
|
+
const cachedPostProperties = await this.redis.get(cacheKey);
|
|
881
|
+
if (cachedPostProperties) {
|
|
882
|
+
this.cachedPostProperties[postId] = JSON.parse(cachedPostProperties);
|
|
883
|
+
this.postPropertiesStatistics.cacheHits++;
|
|
884
|
+
this.postPropertiesStatistics.totalTime += Date.now() - now;
|
|
885
|
+
return this.cachedPostProperties[postId];
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
const postProperties = await this.context.reddit.getPostById(postId).then(post => ({
|
|
889
|
+
authorName: post.authorName,
|
|
890
|
+
title: post.title,
|
|
891
|
+
body: post.body,
|
|
892
|
+
url: post.url,
|
|
893
|
+
createdAt: post.createdAt.getTime(),
|
|
894
|
+
}));
|
|
895
|
+
await this.redis.set(cacheKey, JSON.stringify(postProperties), { expiration: (0, date_fns_1.addDays)(new Date(), 7) });
|
|
896
|
+
this.cachedPostProperties[postId] = postProperties;
|
|
897
|
+
this.postPropertiesStatistics.cacheMisses++;
|
|
898
|
+
this.postPropertiesStatistics.totalTime += Date.now() - now;
|
|
899
|
+
return postProperties;
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
903
|
+
this.evaluationError = `Error fetching post properties for postId ${postId}: ${message}`;
|
|
904
|
+
this.postPropertiesStatistics.errors++;
|
|
905
|
+
this.postPropertiesStatistics.totalTime += Date.now() - now;
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async commentMatchesCondition(comment, condition, history) {
|
|
910
|
+
const conditionMatch = this.postOrCommentMatchesCondition(comment, condition);
|
|
911
|
+
if (!conditionMatch.matched) {
|
|
912
|
+
return { matched: false };
|
|
913
|
+
}
|
|
914
|
+
const matchReasons = [...(conditionMatch.reasons ?? []).map(reason => ({ key: `comment ${reason.key}`, value: reason.value }))];
|
|
915
|
+
if (condition.postId && !condition.postId.some(postId => comment.postId === `t3_${postId}`)) {
|
|
916
|
+
return { matched: false };
|
|
917
|
+
}
|
|
918
|
+
if (condition.isTopLevel !== undefined && condition.isTopLevel !== (0, tid_js_1.isLinkId)(comment.parentId)) {
|
|
919
|
+
return { matched: false };
|
|
920
|
+
}
|
|
921
|
+
if (condition.isCommentOnOwnPost !== undefined) {
|
|
922
|
+
if (!history || !("authorName" in comment)) {
|
|
923
|
+
return { matched: false };
|
|
924
|
+
}
|
|
925
|
+
const posts = this.getPosts();
|
|
926
|
+
const parentPost = posts.find(post => post.id === comment.postId);
|
|
927
|
+
const matchesCriteria = condition.isCommentOnOwnPost === (parentPost?.authorName === comment.authorName);
|
|
928
|
+
if (!matchesCriteria) {
|
|
929
|
+
return { matched: false };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
let postProperties;
|
|
933
|
+
if (condition.postAuthorNameRegex || condition.postTitleRegex || condition.postBodyRegex || condition.postUrlRegex || condition.postCreatedAtAge) {
|
|
934
|
+
postProperties = await this.getPostProperties(comment.postId);
|
|
935
|
+
}
|
|
936
|
+
if (condition.postCreatedAtAge) {
|
|
937
|
+
if (!postProperties) {
|
|
938
|
+
return { matched: false };
|
|
939
|
+
}
|
|
940
|
+
if (!this.matchesAgeCriteria(new Date(postProperties.createdAt), condition.postCreatedAtAge, comment.createdAt instanceof Date ? comment.createdAt : new Date(comment.createdAt))) {
|
|
941
|
+
return { matched: false };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (condition.postAuthorNameRegex) {
|
|
945
|
+
if (!postProperties) {
|
|
946
|
+
return { matched: false };
|
|
947
|
+
}
|
|
948
|
+
if (this.anyRegexMatches(postProperties.authorName, condition.postAuthorNameRegex)) {
|
|
949
|
+
matchReasons.push({ key: "postAuthorNameRegex", value: postProperties.authorName });
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
return { matched: false };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (condition.postTitleRegex) {
|
|
956
|
+
if (!postProperties?.title) {
|
|
957
|
+
return { matched: false };
|
|
958
|
+
}
|
|
959
|
+
if (this.anyRegexMatches(postProperties.title, condition.postTitleRegex)) {
|
|
960
|
+
matchReasons.push({ key: "postTitleRegex", value: postProperties.title });
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
return { matched: false };
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (condition.postBodyRegex) {
|
|
967
|
+
if (!postProperties?.body) {
|
|
968
|
+
return { matched: false };
|
|
969
|
+
}
|
|
970
|
+
if (this.anyRegexMatches(postProperties.body, condition.postBodyRegex)) {
|
|
971
|
+
matchReasons.push({ key: "postBodyRegex", value: postProperties.body });
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
return { matched: false };
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (condition.postUrlRegex) {
|
|
978
|
+
if (!postProperties?.url) {
|
|
979
|
+
return { matched: false };
|
|
980
|
+
}
|
|
981
|
+
if (this.anyRegexMatches(postProperties.url, condition.postUrlRegex)) {
|
|
982
|
+
matchReasons.push({ key: "postUrlRegex", value: postProperties.url });
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
return { matched: false };
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return { matched: true, reasons: matchReasons };
|
|
989
|
+
}
|
|
990
|
+
collectPostConditionsForPreEvalation(criteria) {
|
|
991
|
+
const conditions = [];
|
|
992
|
+
if ("type" in criteria && criteria.type === "post") {
|
|
993
|
+
conditions.push(criteria);
|
|
994
|
+
}
|
|
995
|
+
else if ("every" in criteria) {
|
|
996
|
+
for (const subCriteria of criteria.every) {
|
|
997
|
+
conditions.push(...this.collectPostConditionsForPreEvalation(subCriteria));
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
else if ("some" in criteria) {
|
|
1001
|
+
for (const subCriteria of criteria.some) {
|
|
1002
|
+
conditions.push(...this.collectPostConditionsForPreEvalation(subCriteria));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return conditions;
|
|
1006
|
+
}
|
|
1007
|
+
collectNegatedPostConditionsForPreEvaluation(criteria) {
|
|
1008
|
+
const conditions = [];
|
|
1009
|
+
if ("not" in criteria) {
|
|
1010
|
+
conditions.push(...this.collectPostConditionsForPreEvalation(criteria.not));
|
|
1011
|
+
}
|
|
1012
|
+
else if ("every" in criteria) {
|
|
1013
|
+
for (const subCriteria of criteria.every) {
|
|
1014
|
+
conditions.push(...this.collectNegatedPostConditionsForPreEvaluation(subCriteria));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return conditions.filter(condition => condition.matchesNeeded === undefined || condition.matchesNeeded === 1);
|
|
1018
|
+
}
|
|
1019
|
+
async preEvaluatePost(post) {
|
|
1020
|
+
const groups = this.getBotGroups();
|
|
1021
|
+
let userInfo;
|
|
1022
|
+
for (const group of groups) {
|
|
1023
|
+
if (group.usernameRegex && !this.anyRegexMatches(post.authorName, group.usernameRegex)) {
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
if (group.criteria) {
|
|
1027
|
+
const negativePostConditions = this.collectNegatedPostConditionsForPreEvaluation(group.criteria);
|
|
1028
|
+
let negativeConditionMatched = false;
|
|
1029
|
+
for (const condition of negativePostConditions.filter(condition => condition.matchesNeeded === undefined || condition.matchesNeeded === 1)) {
|
|
1030
|
+
const conditionMatches = this.postMatchesCondition(post, condition);
|
|
1031
|
+
if (conditionMatches.matched) {
|
|
1032
|
+
negativeConditionMatched = true;
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (negativeConditionMatched) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const postConditions = this.collectPostConditionsForPreEvalation(group.criteria);
|
|
1040
|
+
if (!postConditions.some(condition => this.postMatchesCondition(post, condition).matched)) {
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
;
|
|
1045
|
+
if (group.age || group.minCommentKarma || group.maxCommentKarma || group.minLinkKarma || group.maxLinkKarma || group.displayNameRegex || group.nsfw || group.hasRedditPremium || group.hasVerifiedEmail || group.isSubredditModerator) {
|
|
1046
|
+
userInfo ?? (userInfo = await (0, utility_js_1.getAdditionalUserInfo)(post.authorName, this.context));
|
|
1047
|
+
if (!userInfo) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (group.age && !this.matchesAgeCriteria(userInfo.createdAt, group.age)) {
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (group.minCommentKarma !== undefined && userInfo.commentKarma < group.minCommentKarma) {
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
if (group.maxCommentKarma !== undefined && userInfo.commentKarma > group.maxCommentKarma) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (group.minLinkKarma !== undefined && userInfo.linkKarma < group.minLinkKarma) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (group.maxLinkKarma !== undefined && userInfo.linkKarma > group.maxLinkKarma) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
if (group.bioRegex && (!userInfo.userDescription || !this.anyRegexMatches(userInfo.userDescription, group.bioRegex))) {
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (group.displayNameRegex && (!userInfo.displayName || !this.anyRegexMatches(userInfo.displayName, group.displayNameRegex))) {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (group.nsfw !== undefined && userInfo.nsfw !== group.nsfw) {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (group.hasRedditPremium !== undefined && userInfo.hasRedditPremium !== group.hasRedditPremium) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (group.hasVerifiedEmail !== undefined && userInfo.hasVerifiedEmail !== group.hasVerifiedEmail) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (group.isSubredditModerator !== undefined && userInfo.isSubredditModerator !== group.isSubredditModerator) {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
async accountMatchesSocialLinkCriteria(user, group) {
|
|
1089
|
+
const matchReasons = [];
|
|
1090
|
+
if (group.socialLinkRegex) {
|
|
1091
|
+
const userSocialLinks = await this.getSocialLinks(user.username);
|
|
1092
|
+
if (userSocialLinks.length === 0) {
|
|
1093
|
+
return { matched: false };
|
|
1094
|
+
}
|
|
1095
|
+
const matchingSocialLink = userSocialLinks.find(userLink => group.socialLinkRegex && this.anyRegexMatches(userLink.outboundUrl, group.socialLinkRegex));
|
|
1096
|
+
if (matchingSocialLink) {
|
|
1097
|
+
matchReasons.push({ key: "socialLinkRegex", value: matchingSocialLink.outboundUrl });
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
return { matched: false };
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (group.socialLinkTitleRegex) {
|
|
1104
|
+
const userSocialLinks = await this.getSocialLinks(user.username);
|
|
1105
|
+
if (userSocialLinks.length === 0) {
|
|
1106
|
+
return { matched: false };
|
|
1107
|
+
}
|
|
1108
|
+
const matchingSocialLink = userSocialLinks.find(userLink => group.socialLinkTitleRegex && userLink.title && this.anyRegexMatches(userLink.title, group.socialLinkTitleRegex));
|
|
1109
|
+
if (matchingSocialLink) {
|
|
1110
|
+
matchReasons.push({ key: "socialLinkTitleRegex", value: matchingSocialLink.title });
|
|
1111
|
+
}
|
|
1112
|
+
else {
|
|
1113
|
+
return { matched: false };
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (group.hasNoSocialLinks !== undefined) {
|
|
1117
|
+
const userSocialLinks = await this.getSocialLinks(user.username);
|
|
1118
|
+
const hasNoSocialLinks = userSocialLinks.length === 0;
|
|
1119
|
+
if (group.hasNoSocialLinks !== hasNoSocialLinks) {
|
|
1120
|
+
return { matched: false };
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return { matched: true, reasons: matchReasons };
|
|
1124
|
+
}
|
|
1125
|
+
async accountMatchesGroup(user, group, evaluateSocialLinks = true) {
|
|
1126
|
+
if (group.usernameRegex && !this.anyRegexMatches(user.username, group.usernameRegex)) {
|
|
1127
|
+
return { matched: false };
|
|
1128
|
+
}
|
|
1129
|
+
if (group.matchesDefaultUsernameRegex !== undefined) {
|
|
1130
|
+
const defaultUsernameRegex = /^(?:[A-Z][a-z]+[_-]?){2}\d{3,4}$/;
|
|
1131
|
+
const matchesDefaultRegex = defaultUsernameRegex.test(user.username);
|
|
1132
|
+
if (group.matchesDefaultUsernameRegex !== matchesDefaultRegex) {
|
|
1133
|
+
return { matched: false };
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (group.age && !this.matchesAgeCriteria(user.createdAt, group.age)) {
|
|
1137
|
+
return { matched: false };
|
|
1138
|
+
}
|
|
1139
|
+
if (group.maxCommentKarma && user.commentKarma > group.maxCommentKarma) {
|
|
1140
|
+
return { matched: false };
|
|
1141
|
+
}
|
|
1142
|
+
if (group.maxLinkKarma && user.linkKarma > group.maxLinkKarma) {
|
|
1143
|
+
return { matched: false };
|
|
1144
|
+
}
|
|
1145
|
+
if (group.minCommentKarma && user.commentKarma < group.minCommentKarma) {
|
|
1146
|
+
return { matched: false };
|
|
1147
|
+
}
|
|
1148
|
+
if (group.minLinkKarma && user.linkKarma < group.minLinkKarma) {
|
|
1149
|
+
return { matched: false };
|
|
1150
|
+
}
|
|
1151
|
+
if (group.nsfw !== undefined && user.nsfw !== group.nsfw) {
|
|
1152
|
+
return { matched: false };
|
|
1153
|
+
}
|
|
1154
|
+
if (group.hasVerifiedEmail !== undefined && user.hasVerifiedEmail !== group.hasVerifiedEmail) {
|
|
1155
|
+
return { matched: false };
|
|
1156
|
+
}
|
|
1157
|
+
if (group.hasRedditPremium !== undefined && user.isGold !== group.hasRedditPremium) {
|
|
1158
|
+
return { matched: false };
|
|
1159
|
+
}
|
|
1160
|
+
if (group.isSubredditModerator !== undefined && user.isModerator !== group.isSubredditModerator) {
|
|
1161
|
+
return { matched: false };
|
|
1162
|
+
}
|
|
1163
|
+
const matchReasons = [];
|
|
1164
|
+
if (group.bioRegex) {
|
|
1165
|
+
if (!user.userDescription) {
|
|
1166
|
+
return { matched: false };
|
|
1167
|
+
}
|
|
1168
|
+
if (this.anyRegexMatches(user.userDescription, group.bioRegex)) {
|
|
1169
|
+
matchReasons.push({ key: "bioRegex", value: user.userDescription });
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
return { matched: false };
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (group.displayNameRegex) {
|
|
1176
|
+
if (!user.displayName) {
|
|
1177
|
+
return { matched: false };
|
|
1178
|
+
}
|
|
1179
|
+
if (this.anyRegexMatches(user.displayName, group.displayNameRegex)) {
|
|
1180
|
+
matchReasons.push({ key: "displayNameRegex", value: user.displayName });
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
return { matched: false };
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (evaluateSocialLinks) {
|
|
1187
|
+
const socialLinkEvaluationResult = await this.accountMatchesSocialLinkCriteria(user, group);
|
|
1188
|
+
if (!socialLinkEvaluationResult.matched) {
|
|
1189
|
+
return { matched: false };
|
|
1190
|
+
}
|
|
1191
|
+
if (socialLinkEvaluationResult.reasons?.length) {
|
|
1192
|
+
matchReasons.push(...socialLinkEvaluationResult.reasons);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return { matched: true, reasons: matchReasons };
|
|
1196
|
+
}
|
|
1197
|
+
async preEvaluateUser(user) {
|
|
1198
|
+
const botGroups = this.getBotGroups();
|
|
1199
|
+
for (const group of botGroups) {
|
|
1200
|
+
const matches = await this.accountMatchesGroup(user, group);
|
|
1201
|
+
if (matches.matched) {
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
async historyMatchesCriteriaGroup(criteria, inNot = false) {
|
|
1208
|
+
if ("not" in criteria) {
|
|
1209
|
+
const matchesGroup = await this.historyMatchesCriteriaGroup(criteria.not, true);
|
|
1210
|
+
return { matched: !matchesGroup.matched };
|
|
1211
|
+
}
|
|
1212
|
+
else if ("every" in criteria) {
|
|
1213
|
+
// Check "not" groups first to allow for early exits before doing more expensive checks for reasons
|
|
1214
|
+
const notCriteria = criteria.every.filter(subCriteria => "not" in subCriteria);
|
|
1215
|
+
if (notCriteria.length > 0) {
|
|
1216
|
+
const notMatches = await Promise.all(notCriteria.map(subCriteria => this.historyMatchesCriteriaGroup(subCriteria)));
|
|
1217
|
+
if (notMatches.some(result => !result.matched)) {
|
|
1218
|
+
return { matched: false };
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const matchReasons = [];
|
|
1222
|
+
// Check remaining criteria.
|
|
1223
|
+
const remainingCriteria = criteria.every.filter(subCriteria => !("not" in subCriteria));
|
|
1224
|
+
for (const subCriteria of remainingCriteria) {
|
|
1225
|
+
const criteriaResult = await this.historyMatchesCriteriaGroup(subCriteria);
|
|
1226
|
+
if (!criteriaResult.matched) {
|
|
1227
|
+
return { matched: false };
|
|
1228
|
+
}
|
|
1229
|
+
if (criteriaResult.reasons) {
|
|
1230
|
+
matchReasons.push(...criteriaResult.reasons);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return { matched: true, reasons: matchReasons };
|
|
1234
|
+
}
|
|
1235
|
+
else if ("some" in criteria) {
|
|
1236
|
+
const someMatch = await Promise.all(criteria.some.map(subCriteria => this.historyMatchesCriteriaGroup(subCriteria)));
|
|
1237
|
+
if (!someMatch.some(result => result.matched)) {
|
|
1238
|
+
return { matched: false };
|
|
1239
|
+
}
|
|
1240
|
+
return { matched: true, reasons: someMatch.filter(item => item.matched).flatMap(result => result.reasons ?? []) };
|
|
1241
|
+
}
|
|
1242
|
+
else if ("type" in criteria) {
|
|
1243
|
+
const matches = [];
|
|
1244
|
+
const subredditsMatched = new Set();
|
|
1245
|
+
const matchesNeeded = criteria.matchesNeeded ?? 1;
|
|
1246
|
+
const maxMatchesNeeded = Math.max(matchesNeeded, 5);
|
|
1247
|
+
const distinctSubredditsNeeded = criteria.distinctSubsNeeded ?? 1;
|
|
1248
|
+
if (criteria.type === "post") {
|
|
1249
|
+
const posts = this.getPosts();
|
|
1250
|
+
for (const post of posts) {
|
|
1251
|
+
if (subredditsMatched.size >= distinctSubredditsNeeded && matches.length >= maxMatchesNeeded) {
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
if (inNot && matches.length >= matchesNeeded && subredditsMatched.size >= distinctSubredditsNeeded) {
|
|
1255
|
+
break;
|
|
1256
|
+
}
|
|
1257
|
+
const matchResult = this.postMatchesCondition(post, criteria);
|
|
1258
|
+
if (matchResult.matched) {
|
|
1259
|
+
matches.push(matchResult);
|
|
1260
|
+
subredditsMatched.add(post.subredditName);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
else { // comments
|
|
1265
|
+
const comments = this.getComments();
|
|
1266
|
+
const currentSubreddit = this.context.subredditName ?? await this.context.reddit.getCurrentSubredditName();
|
|
1267
|
+
for (const comment of comments) {
|
|
1268
|
+
if (subredditsMatched.size >= distinctSubredditsNeeded && matches.length >= maxMatchesNeeded) {
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
if (inNot && matches.length >= matchesNeeded && subredditsMatched.size >= distinctSubredditsNeeded) {
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
const matchResult = await this.commentMatchesCondition(comment, criteria, this.history);
|
|
1275
|
+
if (matchResult.matched) {
|
|
1276
|
+
matches.push(matchResult);
|
|
1277
|
+
const subredditName = "subredditName" in comment ? comment.subredditName : currentSubreddit;
|
|
1278
|
+
subredditsMatched.add(subredditName);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (matches.length < matchesNeeded) {
|
|
1283
|
+
return { matched: false };
|
|
1284
|
+
}
|
|
1285
|
+
if (subredditsMatched.size < distinctSubredditsNeeded) {
|
|
1286
|
+
return { matched: false };
|
|
1287
|
+
}
|
|
1288
|
+
return { matched: true, reasons: this.uniqueMatchReasons(matches.flatMap(result => result.reasons ?? [])) };
|
|
1289
|
+
}
|
|
1290
|
+
return { matched: false }; // Default case, no specific conditions
|
|
1291
|
+
}
|
|
1292
|
+
logEvaluationTime(startTime, username, groupName) {
|
|
1293
|
+
if (!this.verboseLogging) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const endTime = Date.now();
|
|
1297
|
+
if (endTime - startTime > 500) {
|
|
1298
|
+
console.log(`Evaluation for user ${username} and group took ${endTime - startTime} ms "${groupName}"`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
setSubmitterName(submitterName) {
|
|
1302
|
+
this.submitterName = submitterName;
|
|
1303
|
+
}
|
|
1304
|
+
async evaluate(user) {
|
|
1305
|
+
const botGroups = this.getBotGroups();
|
|
1306
|
+
for (const group of botGroups) {
|
|
1307
|
+
const startTime = Date.now();
|
|
1308
|
+
if (group.submitterName !== undefined) {
|
|
1309
|
+
if (this.submitterName === undefined) {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const validSubmitterNames = new Set(Array.isArray(group.submitterName) ? group.submitterName : [group.submitterName]);
|
|
1313
|
+
if (!validSubmitterNames.has(this.submitterName)) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const accountMatches = await this.accountMatchesGroup(user, group, false);
|
|
1318
|
+
if (!accountMatches.matched) {
|
|
1319
|
+
this.logEvaluationTime(startTime, user.username, group.name);
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
const matchReasons = [...(accountMatches.reasons ?? [])];
|
|
1323
|
+
if (group.criteria) {
|
|
1324
|
+
const historyMatchesGroup = await this.historyMatchesCriteriaGroup(group.criteria);
|
|
1325
|
+
if (!historyMatchesGroup.matched) {
|
|
1326
|
+
this.logEvaluationTime(startTime, user.username, group.name);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
matchReasons.push(...(historyMatchesGroup.reasons ?? []));
|
|
1330
|
+
}
|
|
1331
|
+
const socialLinksMatches = await this.accountMatchesSocialLinkCriteria(user, group);
|
|
1332
|
+
if (!socialLinksMatches.matched) {
|
|
1333
|
+
this.logEvaluationTime(startTime, user.username, group.name);
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
matchReasons.push(...(socialLinksMatches.reasons ?? []));
|
|
1338
|
+
}
|
|
1339
|
+
if (group.hasMoreThanOneCommentOnPosts !== undefined) {
|
|
1340
|
+
const comments = this.getComments();
|
|
1341
|
+
const commentCounts = (0, lodash_1.countBy)(comments.map(comment => comment.postId));
|
|
1342
|
+
const hasMultipleComments = Object.values(commentCounts).some(count => count > 1);
|
|
1343
|
+
if (group.hasMoreThanOneCommentOnPosts !== hasMultipleComments) {
|
|
1344
|
+
this.logEvaluationTime(startTime, user.username, group.name);
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
this.addHitReason({ reason: group.name, details: this.uniqueMatchReasons(matchReasons) });
|
|
1349
|
+
this.logEvaluationTime(startTime, user.username, group.name);
|
|
1350
|
+
}
|
|
1351
|
+
if (this.evaluationError) {
|
|
1352
|
+
console.error(`Evaluation error for user ${user.username}: ${this.evaluationError}`);
|
|
1353
|
+
}
|
|
1354
|
+
if (this.verboseLogging && this.postPropertiesStatistics.totalTime > 1000) {
|
|
1355
|
+
console.log(`Post properties fetching statistics for user ${user.username}:`, this.postPropertiesStatistics);
|
|
1356
|
+
}
|
|
1357
|
+
return this.hitReasons !== undefined && this.hitReasons.length > 0;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
exports.EvaluateBotGroupAdvanced = EvaluateBotGroupAdvanced;
|