@joliegg/moderation 0.6.0 → 0.9.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.
- package/dist/actions.d.ts +28 -0
- package/dist/actions.js +48 -0
- package/dist/audit/emitter.d.ts +26 -0
- package/dist/audit/emitter.js +63 -0
- package/dist/audit/events.d.ts +75 -0
- package/dist/audit/events.js +2 -0
- package/dist/audit/index.d.ts +2 -0
- package/dist/audit/index.js +18 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.js +107 -0
- package/dist/index.d.ts +3 -41
- package/dist/index.js +20 -213
- package/dist/providers/aws.d.ts +11 -0
- package/dist/providers/aws.js +58 -0
- package/dist/providers/google.d.ts +21 -0
- package/dist/providers/google.js +61 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +54 -0
- package/dist/providers/webrisk.d.ts +9 -0
- package/dist/providers/webrisk.js +33 -0
- package/dist/raid/age.d.ts +6 -0
- package/dist/raid/age.js +19 -0
- package/dist/raid/detector.d.ts +56 -0
- package/dist/raid/detector.js +90 -0
- package/dist/raid/index.d.ts +2 -0
- package/dist/raid/index.js +18 -0
- package/dist/rubrics/defaults.d.ts +19 -0
- package/dist/rubrics/defaults.js +32 -0
- package/dist/rubrics/index.d.ts +3 -0
- package/dist/rubrics/index.js +19 -0
- package/dist/rubrics/rubric.d.ts +21 -0
- package/dist/rubrics/rubric.js +57 -0
- package/dist/rubrics/types.d.ts +27 -0
- package/dist/rubrics/types.js +2 -0
- package/dist/spam/cache.d.ts +99 -0
- package/dist/spam/cache.js +210 -0
- package/dist/spam/index.d.ts +1 -0
- package/dist/spam/index.js +17 -0
- package/dist/text/index.d.ts +2 -0
- package/dist/text/index.js +18 -0
- package/dist/text/mentions.d.ts +31 -0
- package/dist/text/mentions.js +55 -0
- package/dist/text/normalize.d.ts +15 -0
- package/dist/text/normalize.js +45 -0
- package/dist/types/config.d.ts +13 -0
- package/dist/types/config.js +2 -0
- package/dist/types/index.d.ts +3 -10
- package/dist/types/index.js +15 -0
- package/package.json +66 -13
- package/src/actions.ts +50 -0
- package/src/audit/emitter.ts +77 -0
- package/src/audit/events.ts +89 -0
- package/src/audit/index.ts +2 -0
- package/src/client.ts +137 -0
- package/src/index.ts +3 -277
- package/src/providers/aws.ts +58 -0
- package/src/providers/google.ts +63 -0
- package/src/providers/openai.ts +64 -0
- package/src/providers/webrisk.ts +30 -0
- package/src/raid/age.ts +19 -0
- package/src/raid/detector.ts +133 -0
- package/src/raid/index.ts +2 -0
- package/src/rubrics/defaults.ts +32 -0
- package/src/rubrics/index.ts +3 -0
- package/src/rubrics/rubric.ts +62 -0
- package/src/rubrics/types.ts +30 -0
- package/src/spam/cache.ts +342 -0
- package/src/spam/index.ts +1 -0
- package/src/text/index.ts +2 -0
- package/src/text/mentions.ts +91 -0
- package/src/text/normalize.ts +43 -0
- package/src/types/config.ts +14 -0
- package/src/types/index.ts +5 -11
- /package/dist/{url-blacklist.json → data/url-blacklist.json} +0 -0
- /package/dist/{url-shorteners.json → data/url-shorteners.json} +0 -0
- /package/src/{url-blacklist.json → data/url-blacklist.json} +0 -0
- /package/src/{url-shorteners.json → data/url-shorteners.json} +0 -0
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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,15 @@
|
|
|
1
|
+
import type { ModerationCategory } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI Moderation API adapter.
|
|
4
|
+
*
|
|
5
|
+
* Uses the free `omni-moderation-latest` endpoint (no token cost,
|
|
6
|
+
* no SDK). Returns `ModerationCategory[]` in the same shape the
|
|
7
|
+
* Google and AWS providers return, so downstream callers can treat
|
|
8
|
+
* every provider uniformly.
|
|
9
|
+
*/
|
|
10
|
+
export declare class OpenAIModerationProvider {
|
|
11
|
+
private apiKey;
|
|
12
|
+
private model;
|
|
13
|
+
constructor(apiKey: string, model?: string);
|
|
14
|
+
moderateText(text: string, minimumConfidence?: number): Promise<ModerationCategory[]>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAIModerationProvider = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* OpenAI Moderation API adapter.
|
|
6
|
+
*
|
|
7
|
+
* Uses the free `omni-moderation-latest` endpoint (no token cost,
|
|
8
|
+
* no SDK). Returns `ModerationCategory[]` in the same shape the
|
|
9
|
+
* Google and AWS providers return, so downstream callers can treat
|
|
10
|
+
* every provider uniformly.
|
|
11
|
+
*/
|
|
12
|
+
class OpenAIModerationProvider {
|
|
13
|
+
apiKey;
|
|
14
|
+
model;
|
|
15
|
+
constructor(apiKey, model = 'omni-moderation-latest') {
|
|
16
|
+
this.apiKey = apiKey;
|
|
17
|
+
this.model = model;
|
|
18
|
+
}
|
|
19
|
+
async moderateText(text, minimumConfidence = 0) {
|
|
20
|
+
if (!this.apiKey) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await fetch('https://api.openai.com/v1/moderations', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ model: this.model, input: text }),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const data = (await response.json());
|
|
41
|
+
const result = data.results?.[0];
|
|
42
|
+
if (!result) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return Object.entries(result.categories)
|
|
46
|
+
.filter(([, flagged]) => flagged)
|
|
47
|
+
.map(([category]) => ({
|
|
48
|
+
category,
|
|
49
|
+
confidence: (result.category_scores[category] ?? 0) * 100,
|
|
50
|
+
}))
|
|
51
|
+
.filter(c => c.confidence >= minimumConfidence);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.OpenAIModerationProvider = OpenAIModerationProvider;
|
|
@@ -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;
|
package/dist/raid/age.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isAccountTooNew = isAccountTooNew;
|
|
4
|
+
/**
|
|
5
|
+
* Is a member's account "too new" for the given threshold?
|
|
6
|
+
*
|
|
7
|
+
* Returns `false` when both timestamps are missing.
|
|
8
|
+
*/
|
|
9
|
+
function isAccountTooNew(joinedTimestamp, createdTimestamp, minAgeDays) {
|
|
10
|
+
// For raids, we care about join time, not account creation
|
|
11
|
+
// Altough we can derive some trust from that too.
|
|
12
|
+
const reference = joinedTimestamp ?? createdTimestamp;
|
|
13
|
+
if (reference === null) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const age = Date.now() - reference;
|
|
17
|
+
const minAgeMs = minAgeDays * 24 * 60 * 60 * 1000;
|
|
18
|
+
return age < minAgeMs;
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface RaidDetectorOptions {
|
|
2
|
+
/** Number of joins within the window that constitutes a raid. Default to 10 (very small community) but this really depends on community size and activity. */
|
|
3
|
+
joinThreshold?: number;
|
|
4
|
+
/** Sliding window in seconds. Default to 60 seconds (1 minute). */
|
|
5
|
+
joinWindow?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface MemberJoin {
|
|
8
|
+
memberId: string;
|
|
9
|
+
joinedTimestamp: number;
|
|
10
|
+
createdTimestamp: number;
|
|
11
|
+
}
|
|
12
|
+
export interface RaidTrackResult {
|
|
13
|
+
isRaid: boolean;
|
|
14
|
+
joinCount: number;
|
|
15
|
+
windowSeconds: number;
|
|
16
|
+
}
|
|
17
|
+
export type EnableResult = 'enabled' | 'already_active' | 'already_enabling';
|
|
18
|
+
/**
|
|
19
|
+
* Platform-agnostic raid detector. Tracks recent joins per guild in a
|
|
20
|
+
* sliding window and surfaces the "raid" signal when the join count
|
|
21
|
+
* crosses the configured threshold.
|
|
22
|
+
*
|
|
23
|
+
* State transitions (`tryEnable`, `disable`) are guarded by an
|
|
24
|
+
* in-memory mutex so concurrent `handleMemberJoin` invocations during
|
|
25
|
+
* a join burst cannot both see "not active" and double-fire the
|
|
26
|
+
* enable side effects.
|
|
27
|
+
*
|
|
28
|
+
* The detector owns the sliding-window state. It does NOT own the
|
|
29
|
+
* enforcement — callers decide what to do when `isRaid` is true
|
|
30
|
+
* (timeout, kick, auto-disable timer, mod-channel alert, etc.).
|
|
31
|
+
*/
|
|
32
|
+
export declare class RaidDetector {
|
|
33
|
+
readonly joinThreshold: number;
|
|
34
|
+
readonly joinWindow: number;
|
|
35
|
+
private state;
|
|
36
|
+
private enabling;
|
|
37
|
+
constructor(options?: RaidDetectorOptions);
|
|
38
|
+
private getState;
|
|
39
|
+
private cleanupJoins;
|
|
40
|
+
/**
|
|
41
|
+
* Record a join and return whether the guild has crossed the raid
|
|
42
|
+
* threshold inside the window. `tryEnable` is a separate call so the
|
|
43
|
+
* caller can act on the raid signal atomically.
|
|
44
|
+
*/
|
|
45
|
+
track(guildId: string, member: MemberJoin): RaidTrackResult;
|
|
46
|
+
isActive(guildId: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Attempt to flip the guild into raid-active state. Returns:
|
|
49
|
+
* - 'enabled' if this call performed the transition
|
|
50
|
+
* - 'already_active' if raid mode was already on
|
|
51
|
+
* - 'already_enabling' if another concurrent call is mid-transition
|
|
52
|
+
*/
|
|
53
|
+
tryEnable(guildId: string): Promise<EnableResult>;
|
|
54
|
+
disable(guildId: string): void;
|
|
55
|
+
getJoinCount(guildId: string, windowSeconds?: number): number;
|
|
56
|
+
}
|