@joliegg/moderation 0.6.0 → 0.8.0

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.
Files changed (53) hide show
  1. package/dist/actions.d.ts +28 -0
  2. package/dist/actions.js +48 -0
  3. package/dist/client.d.ts +19 -0
  4. package/dist/client.js +97 -0
  5. package/dist/index.d.ts +3 -41
  6. package/dist/index.js +20 -213
  7. package/dist/providers/aws.d.ts +11 -0
  8. package/dist/providers/aws.js +58 -0
  9. package/dist/providers/google.d.ts +21 -0
  10. package/dist/providers/google.js +61 -0
  11. package/dist/providers/webrisk.d.ts +9 -0
  12. package/dist/providers/webrisk.js +33 -0
  13. package/dist/raid/age.d.ts +6 -0
  14. package/dist/raid/age.js +19 -0
  15. package/dist/raid/detector.d.ts +56 -0
  16. package/dist/raid/detector.js +88 -0
  17. package/dist/raid/index.d.ts +2 -0
  18. package/dist/raid/index.js +18 -0
  19. package/dist/spam/cache.d.ts +99 -0
  20. package/dist/spam/cache.js +210 -0
  21. package/dist/spam/index.d.ts +1 -0
  22. package/dist/spam/index.js +17 -0
  23. package/dist/text/index.d.ts +2 -0
  24. package/dist/text/index.js +18 -0
  25. package/dist/text/mentions.d.ts +31 -0
  26. package/dist/text/mentions.js +55 -0
  27. package/dist/text/normalize.d.ts +15 -0
  28. package/dist/text/normalize.js +45 -0
  29. package/dist/types/config.d.ts +13 -0
  30. package/dist/types/config.js +2 -0
  31. package/dist/types/index.d.ts +3 -10
  32. package/dist/types/index.js +15 -0
  33. package/package.json +54 -13
  34. package/src/actions.ts +50 -0
  35. package/src/client.ts +121 -0
  36. package/src/index.ts +3 -277
  37. package/src/providers/aws.ts +58 -0
  38. package/src/providers/google.ts +63 -0
  39. package/src/providers/webrisk.ts +30 -0
  40. package/src/raid/age.ts +19 -0
  41. package/src/raid/detector.ts +122 -0
  42. package/src/raid/index.ts +2 -0
  43. package/src/spam/cache.ts +342 -0
  44. package/src/spam/index.ts +1 -0
  45. package/src/text/index.ts +2 -0
  46. package/src/text/mentions.ts +91 -0
  47. package/src/text/normalize.ts +43 -0
  48. package/src/types/config.ts +14 -0
  49. package/src/types/index.ts +5 -11
  50. /package/dist/{url-blacklist.json → data/url-blacklist.json} +0 -0
  51. /package/dist/{url-shorteners.json → data/url-shorteners.json} +0 -0
  52. /package/src/{url-blacklist.json → data/url-blacklist.json} +0 -0
  53. /package/src/{url-shorteners.json → data/url-shorteners.json} +0 -0
@@ -0,0 +1,28 @@
1
+ import type { Severity } from './types';
2
+ /**
3
+ * Moderation actions taxonomy. These should be stable so they can be persisted
4
+ * as a canonical list of actions.
5
+ */
6
+ export declare const ACTION_TYPES: {
7
+ readonly TIMEOUT: "timeout";
8
+ readonly BAN: "ban";
9
+ readonly KICK: "kick";
10
+ readonly WARN: "warn";
11
+ readonly DELETE: "delete";
12
+ readonly RESTRICT: "restrict";
13
+ readonly APPEAL_APPROVE: "appeal_approve";
14
+ readonly APPEAL_DENY: "appeal_deny";
15
+ readonly MENTION_SPAM: "mention_spam";
16
+ readonly NEW_USER_RESTRICT: "new_user_restrict";
17
+ readonly SPAM_DETECTED: "spam_detected";
18
+ readonly ESCALATION: "escalation";
19
+ readonly RAID_DETECTED: "raid_detected";
20
+ readonly RAID_TIMEOUT: "raid_timeout";
21
+ readonly RAID_JOIN: "raid_join";
22
+ readonly PERMISSION_BLOCK: "permission_block";
23
+ readonly UNTIMEOUT: "untimeout";
24
+ readonly UNBAN: "unban";
25
+ };
26
+ export type ActionType = typeof ACTION_TYPES[keyof typeof ACTION_TYPES];
27
+ /** Default severity per action type. */
28
+ export declare const SEVERITY_BY_ACTION: Record<ActionType, Severity>;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SEVERITY_BY_ACTION = exports.ACTION_TYPES = void 0;
4
+ /**
5
+ * Moderation actions taxonomy. These should be stable so they can be persisted
6
+ * as a canonical list of actions.
7
+ */
8
+ exports.ACTION_TYPES = {
9
+ TIMEOUT: 'timeout',
10
+ BAN: 'ban',
11
+ KICK: 'kick',
12
+ WARN: 'warn',
13
+ DELETE: 'delete',
14
+ RESTRICT: 'restrict',
15
+ APPEAL_APPROVE: 'appeal_approve',
16
+ APPEAL_DENY: 'appeal_deny',
17
+ MENTION_SPAM: 'mention_spam',
18
+ NEW_USER_RESTRICT: 'new_user_restrict',
19
+ SPAM_DETECTED: 'spam_detected',
20
+ ESCALATION: 'escalation',
21
+ RAID_DETECTED: 'raid_detected',
22
+ RAID_TIMEOUT: 'raid_timeout',
23
+ RAID_JOIN: 'raid_join',
24
+ PERMISSION_BLOCK: 'permission_block',
25
+ UNTIMEOUT: 'untimeout',
26
+ UNBAN: 'unban',
27
+ };
28
+ /** Default severity per action type. */
29
+ exports.SEVERITY_BY_ACTION = {
30
+ timeout: 'medium',
31
+ ban: 'critical',
32
+ kick: 'high',
33
+ warn: 'low',
34
+ delete: 'low',
35
+ restrict: 'medium',
36
+ appeal_approve: 'low',
37
+ appeal_deny: 'low',
38
+ mention_spam: 'medium',
39
+ new_user_restrict: 'low',
40
+ spam_detected: 'medium',
41
+ escalation: 'high',
42
+ raid_detected: 'critical',
43
+ raid_timeout: 'medium',
44
+ raid_join: 'low',
45
+ permission_block: 'low',
46
+ untimeout: 'low',
47
+ unban: 'low',
48
+ };
@@ -0,0 +1,19 @@
1
+ import type { ModerationConfiguration, ModerationResult } from './types';
2
+ /**
3
+ * Composes text / image / link / audio moderation across the configured
4
+ * providers.
5
+ */
6
+ export declare class ModerationClient {
7
+ private googleLanguage?;
8
+ private googleSpeech?;
9
+ private aws?;
10
+ private webRisk?;
11
+ private banList;
12
+ private urlBlackList;
13
+ constructor(configuration: ModerationConfiguration);
14
+ moderateText(text: string, minimumConfidence?: number): Promise<ModerationResult>;
15
+ moderateImage(url: string, minimumConfidence?: number): Promise<ModerationResult>;
16
+ moderateLink(url: string, allowShorteners?: boolean): Promise<ModerationResult>;
17
+ moderateAudio(url: string, language?: string, minimumConfidence?: number): Promise<ModerationResult>;
18
+ }
19
+ export default ModerationClient;
package/dist/client.js ADDED
@@ -0,0 +1,97 @@
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.ModerationClient = void 0;
7
+ const client_rekognition_1 = require("@aws-sdk/client-rekognition");
8
+ const language_1 = require("@google-cloud/language");
9
+ const speech_1 = require("@google-cloud/speech");
10
+ const google_1 = require("./providers/google");
11
+ const aws_1 = require("./providers/aws");
12
+ const webrisk_1 = require("./providers/webrisk");
13
+ const url_blacklist_json_1 = __importDefault(require("./data/url-blacklist.json"));
14
+ const url_shorteners_json_1 = __importDefault(require("./data/url-shorteners.json"));
15
+ /**
16
+ * Composes text / image / link / audio moderation across the configured
17
+ * providers.
18
+ */
19
+ class ModerationClient {
20
+ googleLanguage;
21
+ googleSpeech;
22
+ aws;
23
+ webRisk;
24
+ banList = [];
25
+ urlBlackList = [];
26
+ constructor(configuration) {
27
+ if (configuration.aws) {
28
+ this.aws = new aws_1.RekognitionProvider(new client_rekognition_1.Rekognition(configuration.aws));
29
+ }
30
+ if (typeof configuration.google?.keyFile === 'string') {
31
+ this.googleLanguage = new google_1.GoogleLanguageProvider(new language_1.LanguageServiceClient({ keyFile: configuration.google.keyFile }));
32
+ this.googleSpeech = new google_1.GoogleSpeechProvider(new speech_1.SpeechClient({ keyFile: configuration.google.keyFile }));
33
+ }
34
+ if (typeof configuration.google?.apiKey === 'string') {
35
+ this.webRisk = new webrisk_1.WebRiskProvider(configuration.google.apiKey);
36
+ }
37
+ if (Array.isArray(configuration.banList)) {
38
+ this.banList = configuration.banList;
39
+ }
40
+ if (Array.isArray(configuration.urlBlackList)) {
41
+ this.urlBlackList = configuration.urlBlackList;
42
+ }
43
+ }
44
+ async moderateText(text, minimumConfidence = 50) {
45
+ const categories = [];
46
+ const normalized = text.toLowerCase();
47
+ const matches = this.banList.filter(w => normalized.includes(w));
48
+ if (matches.length > 0) {
49
+ const words = normalized.split(' ');
50
+ categories.push({ category: 'BAN_LIST', confidence: (matches.length / words.length) * 100 });
51
+ }
52
+ if (!this.googleLanguage) {
53
+ return { source: text, moderation: categories };
54
+ }
55
+ const googleCategories = await this.googleLanguage.moderateText(text, minimumConfidence);
56
+ return { source: text, moderation: [...categories, ...googleCategories] };
57
+ }
58
+ async moderateImage(url, minimumConfidence = 50) {
59
+ if (!this.aws) {
60
+ return { source: url, moderation: [] };
61
+ }
62
+ const moderation = await this.aws.moderateImage(url, minimumConfidence);
63
+ return { source: url, moderation };
64
+ }
65
+ async moderateLink(url, allowShorteners = false) {
66
+ try {
67
+ const domain = new URL(url).hostname;
68
+ if (this.urlBlackList.some(u => url.includes(u))) {
69
+ return { source: url, moderation: [{ category: 'CUSTOM_BLACK_LIST', confidence: 100 }] };
70
+ }
71
+ if (url_blacklist_json_1.default.some(u => u === domain)) {
72
+ return { source: url, moderation: [{ category: 'BLACK_LIST', confidence: 100 }] };
73
+ }
74
+ if (!allowShorteners && url_shorteners_json_1.default.some(u => u === domain)) {
75
+ return { source: url, moderation: [{ category: 'URL_SHORTENER', confidence: 100 }] };
76
+ }
77
+ }
78
+ catch {
79
+ return { source: url, moderation: [] };
80
+ }
81
+ if (!this.webRisk) {
82
+ return { source: url, moderation: [] };
83
+ }
84
+ const moderation = await this.webRisk.checkLink(url);
85
+ return { source: url, moderation };
86
+ }
87
+ async moderateAudio(url, language = 'en-US', minimumConfidence = 50) {
88
+ if (!this.googleSpeech) {
89
+ return { source: url, moderation: [] };
90
+ }
91
+ const buffer = await (0, google_1.fetchAudio)(url);
92
+ const transcription = await this.googleSpeech.transcribe(buffer, language);
93
+ return this.moderateText(transcription, minimumConfidence);
94
+ }
95
+ }
96
+ exports.ModerationClient = ModerationClient;
97
+ exports.default = ModerationClient;
package/dist/index.d.ts CHANGED
@@ -1,41 +1,3 @@
1
- import { ModerationConfiguration, ModerationResult } from './types';
2
- /**
3
- * Moderation Client
4
- *
5
- * @class ModerationClient
6
- */
7
- declare class ModerationClient {
8
- private rekognitionClient?;
9
- private googleLanguageClient?;
10
- private googleSpeechClient?;
11
- private googleAPIKey?;
12
- private banList?;
13
- private urlBlackList?;
14
- /**
15
- *
16
- * @param {ModerationConfiguration} configuration
17
- */
18
- constructor(configuration: ModerationConfiguration);
19
- /**
20
- * Returns a list of moderation categories detected on a text
21
- *
22
- * @param {string} text The text to moderate
23
- * @param {number} [minimumConfidence = 50] The minimum confidence required for a category to be considered
24
- *
25
- * @returns {Promise<ModerationResult>} The list of results that were detected with the minimum confidence specified
26
- */
27
- moderateText(text: string, minimumConfidence?: number): Promise<ModerationResult>;
28
- /**
29
- * Returns a list of moderation categories detected on an image
30
- *
31
- * @param {string} url
32
- * @param {number} [minimumConfidence = 50] The minimum confidence required for a category to be considered
33
- *
34
- *
35
- * @returns {Promise<ModerationResult[]>} The list of results that were detected with the minimum confidence specified
36
- */
37
- moderateImage(url: string, minimumConfidence?: number): Promise<ModerationResult>;
38
- moderateLink(url: string, allowShorteners?: boolean): Promise<ModerationResult>;
39
- moderateAudio(url: string, language?: string, minimumConfidence?: number): Promise<ModerationResult>;
40
- }
41
- export default ModerationClient;
1
+ export { ModerationClient, default } from './client';
2
+ export * from './types';
3
+ export * from './actions';
package/dist/index.js CHANGED
@@ -1,218 +1,25 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
18
  };
5
19
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const axios_1 = __importDefault(require("axios"));
7
- const client_rekognition_1 = require("@aws-sdk/client-rekognition");
8
- const language_1 = require("@google-cloud/language");
9
- const speech_1 = require("@google-cloud/speech");
10
- const sharp_1 = __importDefault(require("sharp"));
11
- const url_blacklist_json_1 = __importDefault(require("./url-blacklist.json"));
12
- const url_shorteners_json_1 = __importDefault(require("./url-shorteners.json"));
13
- const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB in bytes for Rekognition limit
14
- /**
15
- * Moderation Client
16
- *
17
- * @class ModerationClient
18
- */
19
- class ModerationClient {
20
- rekognitionClient;
21
- googleLanguageClient;
22
- googleSpeechClient;
23
- googleAPIKey;
24
- banList = [];
25
- urlBlackList = [];
26
- /**
27
- *
28
- * @param {ModerationConfiguration} configuration
29
- */
30
- constructor(configuration) {
31
- if (configuration.aws) {
32
- this.rekognitionClient = new client_rekognition_1.Rekognition(configuration.aws);
33
- }
34
- if (typeof configuration.google?.keyFile === 'string') {
35
- this.googleLanguageClient = new language_1.LanguageServiceClient({ keyFile: configuration.google.keyFile });
36
- this.googleSpeechClient = new speech_1.SpeechClient({ keyFile: configuration.google.keyFile });
37
- }
38
- if (typeof configuration.google?.apiKey === 'string') {
39
- this.googleAPIKey = configuration.google.apiKey;
40
- }
41
- if (Array.isArray(configuration.banList)) {
42
- this.banList = configuration.banList;
43
- }
44
- if (Array.isArray(configuration.urlBlackList)) {
45
- this.urlBlackList = configuration.urlBlackList;
46
- }
47
- }
48
- /**
49
- * Returns a list of moderation categories detected on a text
50
- *
51
- * @param {string} text The text to moderate
52
- * @param {number} [minimumConfidence = 50] The minimum confidence required for a category to be considered
53
- *
54
- * @returns {Promise<ModerationResult>} The list of results that were detected with the minimum confidence specified
55
- */
56
- async moderateText(text, minimumConfidence = 50) {
57
- const categories = [];
58
- if (Array.isArray(this.banList)) {
59
- const normalizedText = text.toLowerCase();
60
- const matches = this.banList.filter(w => normalizedText.indexOf(w) > -1);
61
- if (matches.length > 0) {
62
- const words = normalizedText.split(' ');
63
- categories.push({
64
- category: 'BAN_LIST',
65
- confidence: (matches.length / words.length) * 100,
66
- });
67
- }
68
- }
69
- if (typeof this.googleLanguageClient === 'undefined') {
70
- return { source: text, moderation: categories };
71
- }
72
- const [result] = await this.googleLanguageClient.moderateText({
73
- document: {
74
- content: text,
75
- type: 'PLAIN_TEXT',
76
- },
77
- });
78
- if (result && 'moderationCategories' in result) {
79
- if (Array.isArray(result.moderationCategories)) {
80
- const results = result.moderationCategories.map(c => ({
81
- category: c.name ?? 'Unknown',
82
- confidence: (c.confidence ?? 0) * 100,
83
- })).filter(c => c.confidence >= minimumConfidence);
84
- return { source: text, moderation: [...categories, ...results] };
85
- }
86
- }
87
- return { source: text, moderation: [] };
88
- }
89
- /**
90
- * Returns a list of moderation categories detected on an image
91
- *
92
- * @param {string} url
93
- * @param {number} [minimumConfidence = 50] The minimum confidence required for a category to be considered
94
- *
95
- *
96
- * @returns {Promise<ModerationResult[]>} The list of results that were detected with the minimum confidence specified
97
- */
98
- async moderateImage(url, minimumConfidence = 50) {
99
- if (typeof this.rekognitionClient === 'undefined') {
100
- return { source: url, moderation: [] };
101
- }
102
- const { data } = await axios_1.default.get(url, { responseType: 'arraybuffer' });
103
- let buffer;
104
- // GIFs will be split into frames
105
- if (url.toLowerCase().indexOf('.gif') > -1) {
106
- buffer = await (0, sharp_1.default)(data, { pages: -1 }).toFormat('png').toBuffer();
107
- }
108
- else if (url.toLowerCase().indexOf('.webp') > -1) {
109
- buffer = await (0, sharp_1.default)(data).toFormat('png').toBuffer();
110
- }
111
- else {
112
- // Download image as binary data
113
- buffer = Buffer.from(data, 'binary');
114
- }
115
- // Ensure image is not larger than 5MB (Rekognition limit)
116
- if (buffer.length > MAX_IMAGE_SIZE) {
117
- try {
118
- // Calculate new dimensions to reduce size
119
- const metadata = await (0, sharp_1.default)(buffer).metadata();
120
- const { width, height } = metadata;
121
- if (typeof width !== 'number' || typeof height !== 'number') {
122
- throw new Error('Invalid image metadata');
123
- }
124
- // Calculate the scaling factor
125
- const scalingFactor = Math.sqrt(MAX_IMAGE_SIZE / buffer.length);
126
- // Calculate new dimensions
127
- const newWidth = Math.floor(width * scalingFactor);
128
- const newHeight = Math.floor(height * scalingFactor);
129
- const resizedBuffer = await (0, sharp_1.default)(buffer)
130
- .resize(Math.round(newWidth), Math.round(newHeight))
131
- .toBuffer();
132
- buffer = resizedBuffer;
133
- }
134
- catch {
135
- // We can't resize the image. We'll skip the resize and try to process it as is
136
- }
137
- }
138
- const { ModerationLabels } = await this.rekognitionClient.detectModerationLabels({
139
- Image: {
140
- Bytes: buffer
141
- },
142
- MinConfidence: minimumConfidence
143
- });
144
- if (Array.isArray(ModerationLabels)) {
145
- const moderation = ModerationLabels.map(l => ({
146
- category: l.Name ?? 'Unknown',
147
- confidence: l.Confidence ?? 0,
148
- }));
149
- return { source: url, moderation };
150
- }
151
- return { source: url, moderation: [] };
152
- }
153
- async moderateLink(url, allowShorteners = false) {
154
- try {
155
- const domain = new URL(url).hostname;
156
- const blacklisted = this.urlBlackList?.some(u => u.indexOf(url) > -1);
157
- if (blacklisted) {
158
- return { source: url, moderation: [{ category: 'CUSTOM_BLACK_LIST', confidence: 100 }] };
159
- }
160
- const globallyBlacklisted = url_blacklist_json_1.default.some(u => u === domain);
161
- if (globallyBlacklisted) {
162
- return { source: url, moderation: [{ category: 'BLACK_LIST', confidence: 100 }] };
163
- }
164
- if (!allowShorteners) {
165
- const isShortened = url_shorteners_json_1.default.some(u => u === domain);
166
- if (isShortened) {
167
- return { source: url, moderation: [{ category: 'URL_SHORTENER', confidence: 100 }] };
168
- }
169
- }
170
- }
171
- catch {
172
- // Invalid URL
173
- return { source: url, moderation: [] };
174
- }
175
- if (typeof this.googleAPIKey !== 'string') {
176
- return { source: url, moderation: [] };
177
- }
178
- const types = [
179
- 'MALWARE',
180
- 'SOCIAL_ENGINEERING',
181
- 'UNWANTED_SOFTWARE',
182
- 'SOCIAL_ENGINEERING_EXTENDED_COVERAGE'
183
- ];
184
- const threatTypes = types.join('&threatTypes=');
185
- const requestUrl = `https://webrisk.googleapis.com/v1/uris:search?threatTypes=${threatTypes}&key=${this.googleAPIKey}`;
186
- const { data } = await axios_1.default.get(`${requestUrl}&uri=${encodeURIComponent(url)}`);
187
- const threats = data?.threat?.threatTypes;
188
- if (Array.isArray(threats)) {
189
- const moderation = threats.map(t => ({
190
- category: t,
191
- confidence: 100,
192
- }));
193
- return { source: url, moderation };
194
- }
195
- return { source: url, moderation: [] };
196
- }
197
- async moderateAudio(url, language = 'en-US', minimumConfidence = 50) {
198
- if (typeof this.googleSpeechClient === 'undefined') {
199
- return { source: url, moderation: [] };
200
- }
201
- const { data } = await axios_1.default.get(url, { responseType: 'arraybuffer' });
202
- const options = {
203
- encoding: 'OGG_OPUS',
204
- sampleRateHertz: 48000,
205
- languageCode: language,
206
- };
207
- const [response] = await this.googleSpeechClient.recognize({
208
- audio: { content: Buffer.from(data, 'binary').toString('base64') },
209
- config: options,
210
- });
211
- if (!Array.isArray(response?.results)) {
212
- return { source: url, moderation: [] };
213
- }
214
- const transcription = response?.results?.map((result) => result.alternatives?.at(0)?.transcript ?? '').join(' ');
215
- return this.moderateText(transcription, minimumConfidence);
216
- }
217
- }
218
- exports.default = ModerationClient;
20
+ exports.default = exports.ModerationClient = void 0;
21
+ var client_1 = require("./client");
22
+ Object.defineProperty(exports, "ModerationClient", { enumerable: true, get: function () { return client_1.ModerationClient; } });
23
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(client_1).default; } });
24
+ __exportStar(require("./types"), exports);
25
+ __exportStar(require("./actions"), exports);
@@ -0,0 +1,11 @@
1
+ import { Rekognition } from '@aws-sdk/client-rekognition';
2
+ import type { ModerationCategory } from '../types';
3
+ /**
4
+ * AWS Rekognition image moderation adapter. Handles GIF and WEBP
5
+ * conversion to PNG and downscales images over the 5 MB Rekognition limit.
6
+ */
7
+ export declare class RekognitionProvider {
8
+ private client;
9
+ constructor(client: Rekognition);
10
+ moderateImage(url: string, minimumConfidence: number): Promise<ModerationCategory[]>;
11
+ }
@@ -0,0 +1,58 @@
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.RekognitionProvider = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const sharp_1 = __importDefault(require("sharp"));
9
+ // AWS Rekognition 5MB inline-image limit
10
+ const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
11
+ /**
12
+ * AWS Rekognition image moderation adapter. Handles GIF and WEBP
13
+ * conversion to PNG and downscales images over the 5 MB Rekognition limit.
14
+ */
15
+ class RekognitionProvider {
16
+ client;
17
+ constructor(client) {
18
+ this.client = client;
19
+ }
20
+ async moderateImage(url, minimumConfidence) {
21
+ const { data } = await axios_1.default.get(url, { responseType: 'arraybuffer' });
22
+ let buffer;
23
+ const lowered = url.toLowerCase();
24
+ if (lowered.includes('.gif')) {
25
+ buffer = await (0, sharp_1.default)(Buffer.from(data), { pages: -1 }).toFormat('png').toBuffer();
26
+ }
27
+ else if (lowered.includes('.webp')) {
28
+ buffer = await (0, sharp_1.default)(Buffer.from(data)).toFormat('png').toBuffer();
29
+ }
30
+ else {
31
+ buffer = Buffer.from(data);
32
+ }
33
+ if (buffer.length > MAX_IMAGE_SIZE) {
34
+ try {
35
+ const metadata = await (0, sharp_1.default)(buffer).metadata();
36
+ const { width, height } = metadata;
37
+ if (typeof width === 'number' && typeof height === 'number') {
38
+ const scalingFactor = Math.sqrt(MAX_IMAGE_SIZE / buffer.length);
39
+ buffer = await (0, sharp_1.default)(buffer)
40
+ .resize(Math.round(width * scalingFactor), Math.round(height * scalingFactor))
41
+ .toBuffer();
42
+ }
43
+ }
44
+ catch {
45
+ // Resize failed, we will use the original buffer.
46
+ }
47
+ }
48
+ const { ModerationLabels } = await this.client.detectModerationLabels({
49
+ Image: { Bytes: buffer },
50
+ MinConfidence: minimumConfidence,
51
+ });
52
+ if (Array.isArray(ModerationLabels)) {
53
+ return ModerationLabels.map(l => ({ category: l.Name ?? 'Unknown', confidence: l.Confidence ?? 0 }));
54
+ }
55
+ return [];
56
+ }
57
+ }
58
+ exports.RekognitionProvider = RekognitionProvider;
@@ -0,0 +1,21 @@
1
+ import { LanguageServiceClient } from '@google-cloud/language';
2
+ import { SpeechClient } from '@google-cloud/speech';
3
+ import type { ModerationCategory } from '../types';
4
+ /**
5
+ * Google Cloud Natural Language moderation adapter.
6
+ */
7
+ export declare class GoogleLanguageProvider {
8
+ private client;
9
+ constructor(client: LanguageServiceClient);
10
+ moderateText(text: string, minimumConfidence: number): Promise<ModerationCategory[]>;
11
+ }
12
+ /**
13
+ * Google Cloud Speech-to-Text adapter. Used for audio transcription
14
+ * before running the transcript through text moderation.
15
+ */
16
+ export declare class GoogleSpeechProvider {
17
+ private client;
18
+ constructor(client: SpeechClient);
19
+ transcribe(audioBuffer: Buffer, languageCode: string): Promise<string>;
20
+ }
21
+ export declare function fetchAudio(url: string): Promise<Buffer>;
@@ -0,0 +1,61 @@
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.GoogleSpeechProvider = exports.GoogleLanguageProvider = void 0;
7
+ exports.fetchAudio = fetchAudio;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ /**
10
+ * Google Cloud Natural Language moderation adapter.
11
+ */
12
+ class GoogleLanguageProvider {
13
+ client;
14
+ constructor(client) {
15
+ this.client = client;
16
+ }
17
+ async moderateText(text, minimumConfidence) {
18
+ const [result] = await this.client.moderateText({
19
+ document: { content: text, type: 'PLAIN_TEXT' },
20
+ });
21
+ if (!result || !('moderationCategories' in result) || !Array.isArray(result.moderationCategories)) {
22
+ return [];
23
+ }
24
+ return result.moderationCategories
25
+ .map(c => ({ category: c.name ?? 'Unknown', confidence: (c.confidence ?? 0) * 100 }))
26
+ .filter(c => c.confidence >= minimumConfidence);
27
+ }
28
+ }
29
+ exports.GoogleLanguageProvider = GoogleLanguageProvider;
30
+ /**
31
+ * Google Cloud Speech-to-Text adapter. Used for audio transcription
32
+ * before running the transcript through text moderation.
33
+ */
34
+ class GoogleSpeechProvider {
35
+ client;
36
+ constructor(client) {
37
+ this.client = client;
38
+ }
39
+ async transcribe(audioBuffer, languageCode) {
40
+ const config = {
41
+ encoding: 'OGG_OPUS',
42
+ sampleRateHertz: 48000,
43
+ languageCode,
44
+ };
45
+ const [response] = await this.client.recognize({
46
+ audio: { content: audioBuffer.toString('base64') },
47
+ config,
48
+ });
49
+ if (!Array.isArray(response?.results)) {
50
+ return '';
51
+ }
52
+ return response.results
53
+ .map((r) => r.alternatives?.at(0)?.transcript ?? '')
54
+ .join(' ');
55
+ }
56
+ }
57
+ exports.GoogleSpeechProvider = GoogleSpeechProvider;
58
+ async function fetchAudio(url) {
59
+ const { data } = await axios_1.default.get(url, { responseType: 'arraybuffer' });
60
+ return Buffer.from(data);
61
+ }
@@ -0,0 +1,9 @@
1
+ import type { ModerationCategory } from '../types';
2
+ /**
3
+ * Google Web Risk link moderation adapter.
4
+ */
5
+ export declare class WebRiskProvider {
6
+ private apiKey;
7
+ constructor(apiKey: string);
8
+ checkLink(url: string): Promise<ModerationCategory[]>;
9
+ }
@@ -0,0 +1,33 @@
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.WebRiskProvider = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const THREAT_TYPES = [
9
+ 'MALWARE',
10
+ 'SOCIAL_ENGINEERING',
11
+ 'UNWANTED_SOFTWARE',
12
+ 'SOCIAL_ENGINEERING_EXTENDED_COVERAGE',
13
+ ];
14
+ /**
15
+ * Google Web Risk link moderation adapter.
16
+ */
17
+ class WebRiskProvider {
18
+ apiKey;
19
+ constructor(apiKey) {
20
+ this.apiKey = apiKey;
21
+ }
22
+ async checkLink(url) {
23
+ const threatTypes = THREAT_TYPES.join('&threatTypes=');
24
+ const requestUrl = `https://webrisk.googleapis.com/v1/uris:search?threatTypes=${threatTypes}&key=${this.apiKey}`;
25
+ const { data } = await axios_1.default.get(`${requestUrl}&uri=${encodeURIComponent(url)}`);
26
+ const threats = data?.threat?.threatTypes;
27
+ if (Array.isArray(threats)) {
28
+ return threats.map(t => ({ category: t, confidence: 100 }));
29
+ }
30
+ return [];
31
+ }
32
+ }
33
+ exports.WebRiskProvider = WebRiskProvider;