@fsvreddit/bot-bouncer-evaluation 0.7.46 → 0.7.48

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.
@@ -3,6 +3,7 @@ import { EvaluateSocialLinks } from "./userEvaluation/EvaluateSocialLinks.js";
3
3
  import { EvaluateBotGroupAdvanced } from "./userEvaluation/EvaluateBotGroupAdvanced.js";
4
4
  import { EvaluateBotGroupAdvancedInternal } from "./userEvaluation/EvaluateBotGroupAdvancedInternal.js";
5
5
  import { EvaluateLinkReuse } from "./userEvaluation/EvaluateLinkReuse.js";
6
+ import { EvaluateRedgifs } from "./userEvaluation/EvaluateRedgifs.js";
6
7
  /**
7
8
  * Array of all evaluators.
8
9
  *
@@ -12,5 +13,5 @@ import { EvaluateLinkReuse } from "./userEvaluation/EvaluateLinkReuse.js";
12
13
  * As a result, order entries to prioritise faster evaluators that do not need to
13
14
  * reference social links or account history first.
14
15
  */
15
- export declare const ALL_EVALUATORS: (typeof EvaluatePinnedPostTitles | typeof EvaluateSocialLinks | typeof EvaluateBotGroupAdvanced | typeof EvaluateLinkReuse)[];
16
+ export declare const ALL_EVALUATORS: (typeof EvaluatePinnedPostTitles | typeof EvaluateSocialLinks | typeof EvaluateBotGroupAdvanced | typeof EvaluateRedgifs | typeof EvaluateLinkReuse)[];
16
17
  export declare const ALL_INTERNAL_EVALUATORS: (typeof EvaluateBotGroupAdvancedInternal)[];
@@ -27,6 +27,7 @@ const EvaluateTitleCopyBot_js_1 = require("./userEvaluation/EvaluateTitleCopyBot
27
27
  const EvaluateTextInNsfwImages_js_1 = require("./userEvaluation/EvaluateTextInNsfwImages.js");
28
28
  const EvaluateRepostBot_js_1 = require("./userEvaluation/EvaluateRepostBot.js");
29
29
  const EvaluateLinkReuse_js_1 = require("./userEvaluation/EvaluateLinkReuse.js");
30
+ const EvaluateRedgifs_js_1 = require("./userEvaluation/EvaluateRedgifs.js");
30
31
  /**
31
32
  * Array of all evaluators.
32
33
  *
@@ -65,6 +66,7 @@ exports.ALL_EVALUATORS = [
65
66
  EvaluateLinkReuse_js_1.EvaluateLinkReuse,
66
67
  EvaluateTextInNsfwImages_js_1.EvaluateTextInNsfwImages,
67
68
  EvaluateRepostBot_js_1.EvaluateRepostBot,
69
+ EvaluateRedgifs_js_1.EvaluateRedgifs,
68
70
  ];
69
71
  // Evaluators that will only run on internal tools and not subreddits that may be out of date.
70
72
  exports.ALL_INTERNAL_EVALUATORS = [
@@ -68,9 +68,16 @@ class EvaluateBadDisplayNameDefinedHandles extends UserEvaluatorBase_js_1.UserEv
68
68
  continue;
69
69
  }
70
70
  }
71
- if (this.getHandles().some(handle => handle === "")) {
71
+ const handles = this.getHandles();
72
+ if (handles.some(handle => handle === "")) {
72
73
  results.push({ severity: "error", message: `Empty handle found in baddisplaynamedefinedhandles` });
73
74
  }
75
+ else {
76
+ const shortHandles = handles.filter(handle => handle.length < 4);
77
+ if (shortHandles.length > 0) {
78
+ results.push({ severity: "error", message: `Handle too short in baddisplaynamedefinedhandles: ${shortHandles.join(", ")}` });
79
+ }
80
+ }
74
81
  return results;
75
82
  }
76
83
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -56,6 +56,12 @@ class EvaluateBioTextDefinedHandles extends UserEvaluatorBase_js_1.UserEvaluator
56
56
  if (handles.some(handle => handle === "")) {
57
57
  results.push({ severity: "error", message: `Empty handle found in biotextdefinedhandles` });
58
58
  }
59
+ else {
60
+ const shortHandles = handles.filter(handle => handle.length < 4);
61
+ if (shortHandles.length > 0) {
62
+ results.push({ severity: "error", message: `Handle too short in biotextdefinedhandles: ${shortHandles.join(", ")}` });
63
+ }
64
+ }
59
65
  return results;
60
66
  }
61
67
  gatherRegexes() {
@@ -51,6 +51,12 @@ class EvaluatePostTitleDefinedHandles extends UserEvaluatorBase_js_1.UserEvaluat
51
51
  if (handles.some(handle => handle === "")) {
52
52
  results.push({ severity: "error", message: `Empty handle found in posttitledefinedhandles` });
53
53
  }
54
+ else {
55
+ const shortHandles = handles.filter(handle => handle.length < 4);
56
+ if (shortHandles.length > 0) {
57
+ results.push({ severity: "error", message: `Handle too short in posttitledefinedhandles: ${shortHandles.join(", ")}` });
58
+ }
59
+ }
54
60
  return results;
55
61
  }
56
62
  gatherRegexes() {
@@ -0,0 +1,19 @@
1
+ import { CommentCreate } from "@devvit/protos";
2
+ import { EvaluatorRegex, UserEvaluatorBase, ValidationIssue } from "./UserEvaluatorBase";
3
+ import { Post } from "@devvit/public-api";
4
+ import { UserExtended } from "@fsvreddit/fsv-devvit-helpers";
5
+ export declare class EvaluateRedgifs extends UserEvaluatorBase {
6
+ name: string;
7
+ shortname: string;
8
+ evaluatorDisabled(): boolean;
9
+ gatherRegexes(): EvaluatorRegex[];
10
+ validateVariables(): ValidationIssue[];
11
+ preEvaluateComment(_: CommentCreate): boolean | Promise<boolean>;
12
+ preEvaluatePost(post: Post): boolean;
13
+ preEvaluateUser(user: UserExtended): boolean;
14
+ private getRedis;
15
+ private getRedgifsBearerToken;
16
+ private getRedgifsVideoIdFromUrl;
17
+ private fetchRedgifsPostInformation;
18
+ evaluate(_user: UserExtended): Promise<boolean>;
19
+ }
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.EvaluateRedgifs = void 0;
7
+ const UserEvaluatorBase_1 = require("./UserEvaluatorBase");
8
+ const evaluatorHelpers_1 = require("./evaluatorHelpers");
9
+ const date_fns_1 = require("date-fns");
10
+ const constants_1 = require("../constants");
11
+ const lodash_1 = __importDefault(require("lodash"));
12
+ class EvaluateRedgifs extends UserEvaluatorBase_1.UserEvaluatorBase {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.name = "Redgifs Bot";
16
+ this.shortname = "redgifs";
17
+ }
18
+ evaluatorDisabled() {
19
+ if (super.evaluatorDisabled()) {
20
+ return true;
21
+ }
22
+ const mainAppOnly = this.getVariable("mainAppOnly", false);
23
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
24
+ if (mainAppOnly) {
25
+ return this.context.appSlug === constants_1.MAIN_APP_NAME;
26
+ }
27
+ return false;
28
+ }
29
+ gatherRegexes() {
30
+ const redgifsUsernameRegexes = this.getVariable("redgifsUsernameRegexes", []);
31
+ return redgifsUsernameRegexes.map(regex => ({
32
+ evaluatorName: this.name,
33
+ regex,
34
+ description: `Redgifs username regex: ${regex}`,
35
+ }));
36
+ }
37
+ validateVariables() {
38
+ const issues = [];
39
+ const redgifsUsernameRegexes = this.getVariable("redgifsUsernameRegexes", []);
40
+ for (const regex of redgifsUsernameRegexes) {
41
+ try {
42
+ new RegExp(regex);
43
+ }
44
+ catch {
45
+ issues.push({
46
+ severity: "error",
47
+ message: `Invalid regex: ${regex}`,
48
+ });
49
+ }
50
+ }
51
+ return issues;
52
+ }
53
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
54
+ preEvaluateComment(_) {
55
+ return false;
56
+ }
57
+ preEvaluatePost(post) {
58
+ return (0, evaluatorHelpers_1.domainFromUrl)(post.url) === "redgifs.com";
59
+ }
60
+ preEvaluateUser(user) {
61
+ const minAgeInDays = this.getVariable("minAgeInDays", 0);
62
+ return user.createdAt < (0, date_fns_1.subDays)(new Date(), minAgeInDays);
63
+ }
64
+ getRedis() {
65
+ return this.context.appSlug === constants_1.MAIN_APP_NAME ? this.context.redis.global : this.context.redis;
66
+ }
67
+ async getRedgifsBearerToken() {
68
+ const cacheKey = "bbe:redgifs:bearerToken";
69
+ const cachedToken = await this.getRedis().get(cacheKey);
70
+ if (cachedToken) {
71
+ return cachedToken;
72
+ }
73
+ const response = await fetch("https://api.redgifs.com/v2/auth/temporary", {
74
+ method: "GET",
75
+ headers: {
76
+ accept: "*/*",
77
+ },
78
+ });
79
+ const data = await response.json();
80
+ const newToken = data.token;
81
+ await this.getRedis().set(cacheKey, newToken, { expiration: (0, date_fns_1.addHours)(new Date(), 1) });
82
+ return newToken;
83
+ }
84
+ getRedgifsVideoIdFromUrl(url) {
85
+ const regex = /^https:\/\/(?:www\.)?redgifs.com\/watch\/([a-zA-Z0-9]+)/;
86
+ const match = regex.exec(url);
87
+ return match ? match[1] : undefined;
88
+ }
89
+ async fetchRedgifsPostInformation(post, bearerToken) {
90
+ const videoId = this.getRedgifsVideoIdFromUrl(post.url);
91
+ if (!videoId) {
92
+ return undefined;
93
+ }
94
+ let videoResponse;
95
+ const cacheKey = `bbe:redgifs:postInformation:${videoId}`;
96
+ const cachedPostInformation = await this.getRedis().get(cacheKey);
97
+ if (cachedPostInformation) {
98
+ videoResponse = JSON.parse(cachedPostInformation);
99
+ }
100
+ else {
101
+ const response = await fetch(`https://api.redgifs.com/v2/gifs/${videoId}`, {
102
+ method: "GET",
103
+ headers: {
104
+ Authorization: `Bearer ${bearerToken}`,
105
+ accept: "*/*",
106
+ },
107
+ });
108
+ if (!response.ok) {
109
+ return undefined;
110
+ }
111
+ videoResponse = await response.json();
112
+ await this.getRedis().set(cacheKey, JSON.stringify(videoResponse), { expiration: (0, date_fns_1.addDays)(new Date(), 1) });
113
+ }
114
+ const { gif } = videoResponse;
115
+ return {
116
+ postId: post.id,
117
+ id: videoId,
118
+ createDate: new Date(gif.createDate * 1000),
119
+ description: gif.description,
120
+ duration: gif.duration,
121
+ hasAudio: gif.hasAudio,
122
+ username: gif.username,
123
+ verified: gif.verified,
124
+ };
125
+ }
126
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
127
+ async evaluate(_user) {
128
+ const lookbackDays = this.getVariable("lookbackDays", 28);
129
+ const noCommentsBeforeDays = this.getVariable("noCommentsBeforeDays", 365);
130
+ const minRedgifsPostsNeeded = this.getVariable("minRedgifsPostsNeeded", 10);
131
+ if (this.getComments({ since: (0, date_fns_1.subDays)(new Date(), noCommentsBeforeDays) }).length > 0) {
132
+ return false;
133
+ }
134
+ const recentPosts = this.getPosts({ since: (0, date_fns_1.subDays)(new Date(), lookbackDays) });
135
+ if (recentPosts.some(post => !this.getRedgifsVideoIdFromUrl(post.url))) {
136
+ return false;
137
+ }
138
+ if (recentPosts.length < minRedgifsPostsNeeded) {
139
+ return false;
140
+ }
141
+ ;
142
+ const latestPosts = recentPosts.slice(0, minRedgifsPostsNeeded);
143
+ const bearerToken = await this.getRedgifsBearerToken();
144
+ const postInformation = lodash_1.default.compact(await Promise.all(latestPosts.map(post => this.fetchRedgifsPostInformation(post, bearerToken))));
145
+ const redgifsDenyList = this.getVariable("redgifsDenyList", []);
146
+ const denylistedUsers = lodash_1.default.uniq(postInformation.filter(info => redgifsDenyList.includes(info.username)).map(info => info.username));
147
+ if (denylistedUsers.length > 0) {
148
+ this.addHitReason(`Denylisted Redgifs accounts found: ${denylistedUsers.join(", ")}`);
149
+ return true;
150
+ }
151
+ const redgifsUsernameRegexes = this.getVariable("redgifsUsernameRegexes", []);
152
+ const minDistinctUsernames = this.getVariable("minDistinctUsernames", 1);
153
+ const usernamesFound = lodash_1.default.uniq(postInformation.map(info => info.username));
154
+ if (usernamesFound.length < minDistinctUsernames) {
155
+ return false;
156
+ }
157
+ if (!postInformation.every(info => redgifsUsernameRegexes.some(regex => new RegExp(regex).test(info.username)))) {
158
+ return false;
159
+ }
160
+ const maxDuration = this.getVariable("maxDuration", 300);
161
+ if (postInformation.some(info => info.duration > maxDuration)) {
162
+ return false;
163
+ }
164
+ this.addHitReason(`All recent Redgifs posts meet criteria. Usernames: ${usernamesFound.join(", ")}`);
165
+ return true;
166
+ }
167
+ }
168
+ exports.EvaluateRedgifs = EvaluateRedgifs;
@@ -23,4 +23,5 @@ export * from "./EvaluateBotGroupAdvancedInternal.js";
23
23
  export * from "./EvaluateTitleCopyBot.js";
24
24
  export * from "./EvaluateWorldTraveller.js";
25
25
  export * from "./EvaluateBotGroupSubmitters.js";
26
+ export * from "./EvaluateRedgifs.js";
26
27
  export * from "./UserEvaluatorBase.js";
@@ -39,4 +39,5 @@ __exportStar(require("./EvaluateBotGroupAdvancedInternal.js"), exports);
39
39
  __exportStar(require("./EvaluateTitleCopyBot.js"), exports);
40
40
  __exportStar(require("./EvaluateWorldTraveller.js"), exports);
41
41
  __exportStar(require("./EvaluateBotGroupSubmitters.js"), exports);
42
+ __exportStar(require("./EvaluateRedgifs.js"), exports);
42
43
  __exportStar(require("./UserEvaluatorBase.js"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fsvreddit/bot-bouncer-evaluation",
3
- "version": "0.7.46",
3
+ "version": "0.7.48",
4
4
  "description": "Evaluation code for Bot Bouncer, a Dev Platform app designed to hunt bots on Reddit",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {