@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.
- package/dist/actions.d.ts +28 -0
- package/dist/actions.js +48 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.js +97 -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/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 +88 -0
- package/dist/raid/index.d.ts +2 -0
- package/dist/raid/index.js +18 -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 +54 -13
- package/src/actions.ts +50 -0
- package/src/client.ts +121 -0
- package/src/index.ts +3 -277
- package/src/providers/aws.ts +58 -0
- package/src/providers/google.ts +63 -0
- package/src/providers/webrisk.ts +30 -0
- package/src/raid/age.ts +19 -0
- package/src/raid/detector.ts +122 -0
- package/src/raid/index.ts +2 -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
|
@@ -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>;
|
package/dist/actions.js
ADDED
|
@@ -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
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
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
|
-
|
|
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,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;
|