@hobenakicoffee/libraries 1.6.0 → 1.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/package.json +4 -1
- package/src/moderation/datasets/bn.ts +710 -0
- package/src/moderation/datasets/index.ts +1 -0
- package/src/moderation/index.ts +3 -0
- package/src/moderation/normalizer.test.ts +172 -0
- package/src/moderation/normalizer.ts +25 -0
- package/src/moderation/profanity-service.test.ts +162 -0
- package/src/moderation/profanity-service.ts +50 -0
- package/src/types/supabase.ts +3 -0
- package/src/utils/check-moderation.test.ts +321 -0
- package/src/utils/check-moderation.ts +38 -0
- package/src/utils/get-social-link.ts +0 -1
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { describe, expect, test, mock, beforeEach } from "bun:test";
|
|
2
|
+
import { checkModeration } from "./check-moderation";
|
|
3
|
+
import type OpenAI from "openai";
|
|
4
|
+
|
|
5
|
+
describe("checkModeration", () => {
|
|
6
|
+
let mockOpenAiClient: OpenAI;
|
|
7
|
+
let mockModerations: any;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockModerations = {
|
|
11
|
+
create: mock(async (params: any) => ({
|
|
12
|
+
results: [
|
|
13
|
+
{
|
|
14
|
+
flagged: false,
|
|
15
|
+
categories: {},
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
})),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
mockOpenAiClient = {
|
|
22
|
+
moderations: mockModerations,
|
|
23
|
+
} as unknown as OpenAI;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns OpenAI result when no profanity detected", async () => {
|
|
27
|
+
mockModerations.create = mock(async () => ({
|
|
28
|
+
results: [
|
|
29
|
+
{
|
|
30
|
+
flagged: true,
|
|
31
|
+
categories: {
|
|
32
|
+
hate: true,
|
|
33
|
+
sexual: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const result = await checkModeration(mockOpenAiClient, "clean text");
|
|
40
|
+
|
|
41
|
+
expect(result.flagged).toBe(true);
|
|
42
|
+
expect(result.source).toBe("openai");
|
|
43
|
+
expect(result.categories?.hate).toBe(true);
|
|
44
|
+
expect(result.categories?.sexual).toBe(false);
|
|
45
|
+
expect(result.error).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("returns flagged false when OpenAI approves text", async () => {
|
|
49
|
+
mockModerations.create = mock(async () => ({
|
|
50
|
+
results: [
|
|
51
|
+
{
|
|
52
|
+
flagged: false,
|
|
53
|
+
categories: {},
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const result = await checkModeration(mockOpenAiClient, "safe text");
|
|
59
|
+
|
|
60
|
+
expect(result.flagged).toBe(false);
|
|
61
|
+
expect(result.source).toBe("openai");
|
|
62
|
+
expect(result.error).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("calls OpenAI moderations API with correct model", async () => {
|
|
66
|
+
mockModerations.create = mock(async (params: any) => ({
|
|
67
|
+
results: [
|
|
68
|
+
{
|
|
69
|
+
flagged: false,
|
|
70
|
+
categories: {},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const testText = "test content";
|
|
76
|
+
await checkModeration(mockOpenAiClient, testText);
|
|
77
|
+
|
|
78
|
+
expect(mockModerations.create).toHaveBeenCalledWith({
|
|
79
|
+
model: "omni-moderation-latest",
|
|
80
|
+
input: testText,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("returns error object when OpenAI API throws", async () => {
|
|
85
|
+
const testError = new Error("API Error");
|
|
86
|
+
mockModerations.create = mock(async () => {
|
|
87
|
+
throw testError;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
91
|
+
|
|
92
|
+
expect(result.flagged).toBe(false);
|
|
93
|
+
expect(result.categories).toBeNull();
|
|
94
|
+
expect(result.error).toBe(testError);
|
|
95
|
+
expect(result.source).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("returns error with stack trace when API fails", async () => {
|
|
99
|
+
const testError = new Error("Network error");
|
|
100
|
+
mockModerations.create = mock(async () => {
|
|
101
|
+
throw testError;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
105
|
+
|
|
106
|
+
expect(typeof result.error).toBe("object");
|
|
107
|
+
expect(result.source).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns proper result structure for OpenAI case", async () => {
|
|
111
|
+
mockModerations.create = mock(async () => ({
|
|
112
|
+
results: [
|
|
113
|
+
{
|
|
114
|
+
flagged: false,
|
|
115
|
+
categories: {},
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
121
|
+
|
|
122
|
+
expect(result).toHaveProperty("flagged");
|
|
123
|
+
expect(result).toHaveProperty("categories");
|
|
124
|
+
expect(result).toHaveProperty("error");
|
|
125
|
+
expect(result).toHaveProperty("source");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("returns proper result structure for error case", async () => {
|
|
129
|
+
mockModerations.create = mock(async () => {
|
|
130
|
+
throw new Error("Test error");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
134
|
+
|
|
135
|
+
expect(result).toHaveProperty("flagged");
|
|
136
|
+
expect(result).toHaveProperty("categories");
|
|
137
|
+
expect(result).toHaveProperty("error");
|
|
138
|
+
expect(result).toHaveProperty("source");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("handles OpenAI result with null categories", async () => {
|
|
142
|
+
mockModerations.create = mock(async () => ({
|
|
143
|
+
results: [
|
|
144
|
+
{
|
|
145
|
+
flagged: false,
|
|
146
|
+
categories: null,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
152
|
+
|
|
153
|
+
expect(result.categories).toBeNull();
|
|
154
|
+
expect(result.source).toBe("openai");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("handles empty OpenAI results array gracefully", async () => {
|
|
158
|
+
mockModerations.create = mock(async () => ({
|
|
159
|
+
results: [],
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
163
|
+
|
|
164
|
+
expect(result.flagged).toBe(false);
|
|
165
|
+
expect(result.categories).toBeNull();
|
|
166
|
+
expect(result.source).toBe("openai");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("handles empty text input", async () => {
|
|
170
|
+
mockModerations.create = mock(async () => ({
|
|
171
|
+
results: [
|
|
172
|
+
{
|
|
173
|
+
flagged: false,
|
|
174
|
+
categories: {},
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
const result = await checkModeration(mockOpenAiClient, "");
|
|
180
|
+
|
|
181
|
+
expect(result.source).toBe("openai");
|
|
182
|
+
expect(result.error).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("handles special characters in text", async () => {
|
|
186
|
+
mockModerations.create = mock(async () => ({
|
|
187
|
+
results: [
|
|
188
|
+
{
|
|
189
|
+
flagged: false,
|
|
190
|
+
categories: {},
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
const result = await checkModeration(mockOpenAiClient, "test!@#$%^&*()");
|
|
196
|
+
|
|
197
|
+
expect(result.source).toBe("openai");
|
|
198
|
+
expect(result.error).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("handles very long text", async () => {
|
|
202
|
+
mockModerations.create = mock(async () => ({
|
|
203
|
+
results: [
|
|
204
|
+
{
|
|
205
|
+
flagged: false,
|
|
206
|
+
categories: {},
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
const longText = "test ".repeat(10000);
|
|
212
|
+
const result = await checkModeration(mockOpenAiClient, longText);
|
|
213
|
+
|
|
214
|
+
expect(result.source).toBe("openai");
|
|
215
|
+
expect(result.error).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("returns source as null on error", async () => {
|
|
219
|
+
mockModerations.create = mock(async () => {
|
|
220
|
+
throw new Error("API Error");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
224
|
+
|
|
225
|
+
expect(result.source).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("returns flagged as false when error occurs", async () => {
|
|
229
|
+
mockModerations.create = mock(async () => {
|
|
230
|
+
throw new Error("API Error");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
234
|
+
|
|
235
|
+
expect(result.flagged).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("handles multiple category flags", async () => {
|
|
239
|
+
mockModerations.create = mock(async () => ({
|
|
240
|
+
results: [
|
|
241
|
+
{
|
|
242
|
+
flagged: true,
|
|
243
|
+
categories: {
|
|
244
|
+
hate: true,
|
|
245
|
+
sexual: true,
|
|
246
|
+
violence: true,
|
|
247
|
+
harassment: false,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
254
|
+
|
|
255
|
+
expect(result.flagged).toBe(true);
|
|
256
|
+
expect(result.categories?.hate).toBe(true);
|
|
257
|
+
expect(result.categories?.sexual).toBe(true);
|
|
258
|
+
expect(result.categories?.violence).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("passes input to OpenAI exactly as provided", async () => {
|
|
262
|
+
mockModerations.create = mock(async (params: any) => ({
|
|
263
|
+
results: [
|
|
264
|
+
{
|
|
265
|
+
flagged: false,
|
|
266
|
+
categories: {},
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
const customText = "Custom text with special @#$ chars";
|
|
272
|
+
await checkModeration(mockOpenAiClient, customText);
|
|
273
|
+
|
|
274
|
+
const callArgs = mockModerations.create.mock.calls[0][0];
|
|
275
|
+
expect(callArgs.input).toBe(customText);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("preserves error details in result", async () => {
|
|
279
|
+
const errorMessage = "Specific API Error";
|
|
280
|
+
mockModerations.create = mock(async () => {
|
|
281
|
+
throw new Error(errorMessage);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
285
|
+
|
|
286
|
+
expect((result.error as Error).message).toBe(errorMessage);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("handles undefined OpenAI result fields", async () => {
|
|
290
|
+
mockModerations.create = mock(async () => ({
|
|
291
|
+
results: [
|
|
292
|
+
{
|
|
293
|
+
flagged: undefined,
|
|
294
|
+
categories: undefined,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
const result = await checkModeration(mockOpenAiClient, "test");
|
|
300
|
+
|
|
301
|
+
expect(result.flagged).toBe(false);
|
|
302
|
+
expect(result.categories).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("correctly identifies moderation source as openai", async () => {
|
|
306
|
+
mockModerations.create = mock(async () => ({
|
|
307
|
+
results: [
|
|
308
|
+
{
|
|
309
|
+
flagged: false,
|
|
310
|
+
categories: {},
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
const result = await checkModeration(mockOpenAiClient, "clean content");
|
|
316
|
+
|
|
317
|
+
expect(result.source).toBe("openai");
|
|
318
|
+
expect(result.source).not.toBe("profanity");
|
|
319
|
+
expect(result.source).not.toBeNull();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
import { moderateText } from "../moderation";
|
|
3
|
+
|
|
4
|
+
export async function checkModeration(openaiClient: OpenAI, text: string) {
|
|
5
|
+
try {
|
|
6
|
+
const profanityResult = moderateText(text);
|
|
7
|
+
|
|
8
|
+
if (!profanityResult.isAllowed) {
|
|
9
|
+
return {
|
|
10
|
+
flagged: true,
|
|
11
|
+
categories: null,
|
|
12
|
+
error: null,
|
|
13
|
+
source: "profanity" as const,
|
|
14
|
+
profaneWords: profanityResult.matched,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const moderation = await openaiClient.moderations.create({
|
|
19
|
+
model: "omni-moderation-latest",
|
|
20
|
+
input: text,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = moderation.results[0];
|
|
24
|
+
return {
|
|
25
|
+
flagged: result?.flagged ?? false,
|
|
26
|
+
categories: result?.categories ?? null,
|
|
27
|
+
error: null,
|
|
28
|
+
source: "openai" as const,
|
|
29
|
+
};
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return {
|
|
32
|
+
flagged: false,
|
|
33
|
+
categories: null,
|
|
34
|
+
error: err,
|
|
35
|
+
source: null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { SupporterPlatforms, type SupporterPlatform } from "../constants";
|
|
2
2
|
|
|
3
3
|
export function getSocialLink(username?: string, platform?: SupporterPlatform) {
|
|
4
|
-
console.log("getSocialLink called with:", { username, platform });
|
|
5
4
|
if (!username || !platform) return null;
|
|
6
5
|
|
|
7
6
|
const sanitizedUsername = encodeURIComponent(
|
package/src/utils/index.ts
CHANGED