@kkuffour/solid-moderation-plugin 0.2.3 → 0.3.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/.data/.internal/idp/keys/cookie-secret$.json +1 -0
- package/.data/.internal/idp/keys/jwks$.json +1 -0
- package/.data/.internal/setup/current-base-url$.json +1 -0
- package/PLUGIN_DEVELOPER_GUIDE.md +213 -0
- package/README.md +25 -28
- package/components/context.jsonld +11 -0
- package/config/default.json +14 -28
- package/dist/ModerationHandler.d.ts +21 -0
- package/dist/ModerationHandler.d.ts.map +1 -0
- package/dist/ModerationHandler.js +158 -0
- package/dist/ModerationHandler.js.map +1 -0
- package/dist/ModerationHandler.jsonld +126 -0
- package/dist/components/components.jsonld +4 -9
- package/dist/components/context.jsonld +6 -245
- package/dist/index.d.ts +1 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/providers/SightEngineProvider.jsonld +2 -2
- package/package.json +10 -10
- package/src/ModerationHandler.ts +189 -0
- package/src/index.ts +1 -6
- package/ARCHITECTURE.md +0 -52
- package/CONFIG-GUIDE.md +0 -49
- package/DEVELOPMENT.md +0 -129
- package/ENV-VARIABLES.md +0 -137
- package/INSTALLATION.md +0 -90
- package/MIGRATION.md +0 -81
- package/PRODUCTION.md +0 -186
- package/PUBLISHING.md +0 -104
- package/TESTING.md +0 -93
- package/css-moderation-spec.md +0 -1422
- package/dist/ModerationConfig.d.ts +0 -16
- package/dist/ModerationConfig.d.ts.map +0 -1
- package/dist/ModerationConfig.js +0 -18
- package/dist/ModerationConfig.js.map +0 -1
- package/dist/ModerationConfig.jsonld +0 -66
- package/dist/ModerationMixin.d.ts +0 -13
- package/dist/ModerationMixin.d.ts.map +0 -1
- package/dist/ModerationMixin.js +0 -136
- package/dist/ModerationMixin.js.map +0 -1
- package/dist/ModerationMixin.jsonld +0 -180
- package/dist/ModerationOperationHandler.d.ts +0 -16
- package/dist/ModerationOperationHandler.d.ts.map +0 -1
- package/dist/ModerationOperationHandler.js +0 -45
- package/dist/ModerationOperationHandler.js.map +0 -1
- package/dist/ModerationOperationHandler.jsonld +0 -140
- package/dist/ModerationRecord.d.ts +0 -20
- package/dist/ModerationRecord.d.ts.map +0 -1
- package/dist/ModerationRecord.js +0 -3
- package/dist/ModerationRecord.js.map +0 -1
- package/dist/ModerationRecord.jsonld +0 -59
- package/dist/ModerationResourceStore.d.ts +0 -30
- package/dist/ModerationResourceStore.d.ts.map +0 -1
- package/dist/ModerationResourceStore.js +0 -167
- package/dist/ModerationResourceStore.js.map +0 -1
- package/dist/ModerationResourceStore.jsonld +0 -157
- package/dist/ModerationStore.d.ts +0 -12
- package/dist/ModerationStore.d.ts.map +0 -1
- package/dist/ModerationStore.js +0 -37
- package/dist/ModerationStore.js.map +0 -1
- package/dist/ModerationStore.jsonld +0 -59
- package/dist/util/GuardedStream.d.ts +0 -33
- package/dist/util/GuardedStream.d.ts.map +0 -1
- package/dist/util/GuardedStream.js +0 -89
- package/dist/util/GuardedStream.js.map +0 -1
- package/simple-test.json +0 -7
- package/src/ModerationConfig.ts +0 -29
- package/src/ModerationMixin.ts +0 -153
- package/src/ModerationOperationHandler.ts +0 -64
- package/src/ModerationRecord.ts +0 -19
- package/src/ModerationResourceStore.ts +0 -227
- package/src/ModerationStore.ts +0 -41
- package/src/util/GuardedStream.ts +0 -101
package/dist/ModerationStore.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ModerationStore = void 0;
|
|
4
|
-
const node_fs_1 = require("node:fs");
|
|
5
|
-
const node_path_1 = require("node:path");
|
|
6
|
-
const community_server_1 = require("@solid/community-server");
|
|
7
|
-
/**
|
|
8
|
-
* Service for storing moderation violation records.
|
|
9
|
-
*/
|
|
10
|
-
class ModerationStore {
|
|
11
|
-
constructor(storePath = './data/moderation-logs') {
|
|
12
|
-
this.logger = (0, community_server_1.getLoggerFor)(this);
|
|
13
|
-
this.storePath = storePath;
|
|
14
|
-
}
|
|
15
|
-
async recordViolation(record) {
|
|
16
|
-
try {
|
|
17
|
-
await node_fs_1.promises.mkdir(this.storePath, { recursive: true });
|
|
18
|
-
const fullRecord = {
|
|
19
|
-
...record,
|
|
20
|
-
id: this.generateId(),
|
|
21
|
-
timestamp: new Date(),
|
|
22
|
-
};
|
|
23
|
-
const filename = `${new Date().toISOString().split('T')[0]}.jsonl`;
|
|
24
|
-
const filepath = (0, node_path_1.join)(this.storePath, filename);
|
|
25
|
-
const logEntry = `${JSON.stringify(fullRecord)}\n`;
|
|
26
|
-
await node_fs_1.promises.appendFile(filepath, logEntry);
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
this.logger.error(`Failed to record moderation violation: ${String(error)}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
generateId() {
|
|
33
|
-
return Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
exports.ModerationStore = ModerationStore;
|
|
37
|
-
//# sourceMappingURL=ModerationStore.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ModerationStore.js","sourceRoot":"","sources":["../src/ModerationStore.ts"],"names":[],"mappings":";;;AAAA,qCAAyC;AACzC,yCAAiC;AACjC,8DAAuD;AAGvD;;GAEG;AACH,MAAa,eAAe;IAK1B,YAAmB,SAAS,GAAG,wBAAwB;QAJpC,WAAM,GAAG,IAAA,+BAAY,EAAC,IAAI,CAAC,CAAC;QAK7C,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAEM,KAAK,CAAC,eAAe,CAAC,MAAkD;QAC7E,IAAI,CAAC;YACH,MAAM,kBAAE,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAEpD,MAAM,UAAU,GAAqB;gBACnC,GAAG,MAAM;gBACT,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE;gBACrB,SAAS,EAAE,IAAI,IAAI,EAAE;aACtB,CAAC;YAEF,MAAM,QAAQ,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;YACnE,MAAM,QAAQ,GAAG,IAAA,gBAAI,EAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;YAEnD,MAAM,kBAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,0CAA0C,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAEO,UAAU;QAChB,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACvE,CAAC;CACF;AAhCD,0CAgCC"}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"@context": [
|
|
3
|
-
"https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
|
|
4
|
-
"https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld"
|
|
5
|
-
],
|
|
6
|
-
"@id": "npmd:@kkuffour/solid-moderation-plugin",
|
|
7
|
-
"components": [
|
|
8
|
-
{
|
|
9
|
-
"@id": "ksmp:dist/ModerationStore.jsonld#ModerationStore",
|
|
10
|
-
"@type": "Class",
|
|
11
|
-
"requireElement": "ModerationStore",
|
|
12
|
-
"comment": "Service for storing moderation violation records.",
|
|
13
|
-
"parameters": [
|
|
14
|
-
{
|
|
15
|
-
"@id": "ksmp:dist/ModerationStore.jsonld#ModerationStore_storePath",
|
|
16
|
-
"range": {
|
|
17
|
-
"@type": "ParameterRangeUnion",
|
|
18
|
-
"parameterRangeElements": [
|
|
19
|
-
"xsd:string",
|
|
20
|
-
{
|
|
21
|
-
"@type": "ParameterRangeUndefined"
|
|
22
|
-
}
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
],
|
|
27
|
-
"memberFields": [
|
|
28
|
-
{
|
|
29
|
-
"@id": "ksmp:dist/ModerationStore.jsonld#ModerationStore__member_logger",
|
|
30
|
-
"memberFieldName": "logger",
|
|
31
|
-
"range": {
|
|
32
|
-
"@type": "ParameterRangeWildcard"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"@id": "ksmp:dist/ModerationStore.jsonld#ModerationStore__member_storePath",
|
|
37
|
-
"memberFieldName": "storePath"
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"@id": "ksmp:dist/ModerationStore.jsonld#ModerationStore__member_constructor",
|
|
41
|
-
"memberFieldName": "constructor"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"@id": "ksmp:dist/ModerationStore.jsonld#ModerationStore__member_recordViolation",
|
|
45
|
-
"memberFieldName": "recordViolation"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"@id": "ksmp:dist/ModerationStore.jsonld#ModerationStore__member_generateId",
|
|
49
|
-
"memberFieldName": "generateId"
|
|
50
|
-
}
|
|
51
|
-
],
|
|
52
|
-
"constructorArguments": [
|
|
53
|
-
{
|
|
54
|
-
"@id": "ksmp:dist/ModerationStore.jsonld#ModerationStore_storePath"
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
declare const guardedErrors: unique symbol;
|
|
2
|
-
declare const guardedTimeout: unique symbol;
|
|
3
|
-
declare class Guard {
|
|
4
|
-
private [guardedErrors];
|
|
5
|
-
private [guardedTimeout]?;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* A stream that is guarded from emitting errors when there are no listeners.
|
|
9
|
-
* If an error occurs while no listener is attached,
|
|
10
|
-
* it will store the error and emit it once a listener is added (or a timeout occurs).
|
|
11
|
-
*/
|
|
12
|
-
export type Guarded<T extends NodeJS.EventEmitter = NodeJS.EventEmitter> = T & Guard;
|
|
13
|
-
/**
|
|
14
|
-
* Determines whether the stream is guarded against emitting errors.
|
|
15
|
-
*/
|
|
16
|
-
export declare function isGuarded<T extends NodeJS.EventEmitter>(stream: T): stream is Guarded<T>;
|
|
17
|
-
/**
|
|
18
|
-
* Makes sure that listeners always receive the error event of a stream,
|
|
19
|
-
* even if it was thrown before the listener was attached.
|
|
20
|
-
*
|
|
21
|
-
* When guarding a stream it is assumed that error listeners already attached should be ignored,
|
|
22
|
-
* only error listeners attached after the stream is guarded will prevent an error from being logged.
|
|
23
|
-
*
|
|
24
|
-
* If the input is already guarded the guard will be reset,
|
|
25
|
-
* which means ignoring error listeners already attached.
|
|
26
|
-
*
|
|
27
|
-
* @param stream - Stream that can potentially throw an error.
|
|
28
|
-
*
|
|
29
|
-
* @returns The stream.
|
|
30
|
-
*/
|
|
31
|
-
export declare function guardStream<T extends NodeJS.EventEmitter>(stream: T): Guarded<T>;
|
|
32
|
-
export {};
|
|
33
|
-
//# sourceMappingURL=GuardedStream.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GuardedStream.d.ts","sourceRoot":"","sources":["../../src/util/GuardedStream.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,aAAa,eAA0B,CAAC;AAC9C,QAAA,MAAM,cAAc,eAA2B,CAAC;AAGhD,cAAM,KAAK;IAET,QAAgB,CAAC,aAAa,CAAC,CAAU;IACzC,OAAO,CAAC,CAAC,cAAc,CAAC,CAAC,CAAiB;CAC3C;AAED;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,IAAI,CAAC,GAAG,KAAK,CAAC;AAErF;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,CAExF;AA+CD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAYhF"}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isGuarded = isGuarded;
|
|
4
|
-
exports.guardStream = guardStream;
|
|
5
|
-
const community_server_1 = require("@solid/community-server");
|
|
6
|
-
const logger = (0, community_server_1.getLoggerFor)('GuardedStream');
|
|
7
|
-
// Using symbols to make sure we don't override existing parameters
|
|
8
|
-
const guardedErrors = Symbol('guardedErrors');
|
|
9
|
-
const guardedTimeout = Symbol('guardedTimeout');
|
|
10
|
-
// Private fields for guarded streams
|
|
11
|
-
class Guard {
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Determines whether the stream is guarded against emitting errors.
|
|
15
|
-
*/
|
|
16
|
-
function isGuarded(stream) {
|
|
17
|
-
return typeof stream[guardedErrors] === 'object';
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Callback that is used when a stream emits an error and no other error listener is attached.
|
|
21
|
-
* Used to store the error and start the logger timer.
|
|
22
|
-
*
|
|
23
|
-
* It is important that this listener always remains attached for edge cases where an error listener gets removed
|
|
24
|
-
* and the number of error listeners is checked immediately afterwards.
|
|
25
|
-
* See https://github.com/CommunitySolidServer/CommunitySolidServer/pull/462#issuecomment-758013492 .
|
|
26
|
-
*/
|
|
27
|
-
function guardingErrorListener(error) {
|
|
28
|
-
// Only fall back to this if no new listeners are attached since guarding started.
|
|
29
|
-
const errorListeners = this.listeners('error');
|
|
30
|
-
if (errorListeners.at(-1) === guardingErrorListener) {
|
|
31
|
-
this[guardedErrors].push(error);
|
|
32
|
-
if (!this[guardedTimeout]) {
|
|
33
|
-
this[guardedTimeout] = setTimeout(() => {
|
|
34
|
-
logger.error(`No error listener was attached but error was thrown: ${error.message}`);
|
|
35
|
-
}, 1000);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Callback that is used when a new listener is attached and there are errors that were not emitted yet.
|
|
41
|
-
*/
|
|
42
|
-
function emitStoredErrors(event, func) {
|
|
43
|
-
if (event === 'error' && func !== guardingErrorListener) {
|
|
44
|
-
// Cancel an error timeout
|
|
45
|
-
if (this[guardedTimeout]) {
|
|
46
|
-
clearTimeout(this[guardedTimeout]);
|
|
47
|
-
this[guardedTimeout] = undefined;
|
|
48
|
-
}
|
|
49
|
-
// Emit any errors that were guarded
|
|
50
|
-
const errors = this[guardedErrors];
|
|
51
|
-
if (errors.length > 0) {
|
|
52
|
-
this[guardedErrors] = [];
|
|
53
|
-
setImmediate(() => {
|
|
54
|
-
for (const error of errors) {
|
|
55
|
-
this.emit('error', error);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Makes sure that listeners always receive the error event of a stream,
|
|
63
|
-
* even if it was thrown before the listener was attached.
|
|
64
|
-
*
|
|
65
|
-
* When guarding a stream it is assumed that error listeners already attached should be ignored,
|
|
66
|
-
* only error listeners attached after the stream is guarded will prevent an error from being logged.
|
|
67
|
-
*
|
|
68
|
-
* If the input is already guarded the guard will be reset,
|
|
69
|
-
* which means ignoring error listeners already attached.
|
|
70
|
-
*
|
|
71
|
-
* @param stream - Stream that can potentially throw an error.
|
|
72
|
-
*
|
|
73
|
-
* @returns The stream.
|
|
74
|
-
*/
|
|
75
|
-
function guardStream(stream) {
|
|
76
|
-
const guarded = stream;
|
|
77
|
-
if (isGuarded(stream)) {
|
|
78
|
-
// This makes sure the guarding error listener is the last one in the list again
|
|
79
|
-
guarded.removeListener('error', guardingErrorListener);
|
|
80
|
-
guarded.on('error', guardingErrorListener);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
guarded[guardedErrors] = [];
|
|
84
|
-
guarded.on('error', guardingErrorListener);
|
|
85
|
-
guarded.on('newListener', emitStoredErrors);
|
|
86
|
-
}
|
|
87
|
-
return guarded;
|
|
88
|
-
}
|
|
89
|
-
//# sourceMappingURL=GuardedStream.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GuardedStream.js","sourceRoot":"","sources":["../../src/util/GuardedStream.ts"],"names":[],"mappings":";;AAyBA,8BAEC;AA6DD,kCAYC;AApGD,8DAAuD;AAEvD,MAAM,MAAM,GAAG,IAAA,+BAAY,EAAC,eAAe,CAAC,CAAC;AAE7C,mEAAmE;AACnE,MAAM,aAAa,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;AAC9C,MAAM,cAAc,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAEhD,qCAAqC;AACrC,MAAM,KAAK;CAIV;AASD;;GAEG;AACH,SAAgB,SAAS,CAAgC,MAAS;IAChE,OAAO,OAAQ,MAA6B,CAAC,aAAa,CAAC,KAAK,QAAQ,CAAC;AAC3E,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,qBAAqB,CAAgB,KAAY;IACxD,kFAAkF;IAClF,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,qBAAqB,EAAE,CAAC;QACpD,IAAI,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,cAAc,CAAC,GAAG,UAAU,CAAC,GAAS,EAAE;gBAC3C,MAAM,CAAC,KAAK,CAAC,wDAAwD,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACxF,CAAC,EAAE,IAAI,CAAC,CAAC;QACX,CAAC;IACH,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,gBAAgB,CAAgB,KAAa,EAAE,IAA4B;IAClF,IAAI,KAAK,KAAK,OAAO,IAAI,IAAI,KAAK,qBAAqB,EAAE,CAAC;QACxD,0BAA0B;QAC1B,IAAI,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC;YACzB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;YACnC,IAAI,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;QACnC,CAAC;QAED,oCAAoC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC;QACnC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;YACzB,YAAY,CAAC,GAAS,EAAE;gBACtB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;oBAC3B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAgB,WAAW,CAAgC,MAAS;IAClE,MAAM,OAAO,GAAG,MAAoB,CAAC;IACrC,IAAI,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;QACtB,gFAAgF;QAChF,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;QACvD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;IAC7C,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC;QAC5B,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;QAC3C,OAAO,CAAC,EAAE,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/simple-test.json
DELETED
package/src/ModerationConfig.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export interface ModerationConfig {
|
|
2
|
-
enabled: boolean;
|
|
3
|
-
auditLoggingEnabled: boolean;
|
|
4
|
-
auditLoggingStorePath: string;
|
|
5
|
-
sightEngineApiUser: string;
|
|
6
|
-
sightEngineApiSecret: string;
|
|
7
|
-
imagesEnabled: boolean;
|
|
8
|
-
textEnabled: boolean;
|
|
9
|
-
videoEnabled: boolean;
|
|
10
|
-
imageNudityThreshold: number;
|
|
11
|
-
textSexualThreshold: number;
|
|
12
|
-
textToxicThreshold: number;
|
|
13
|
-
videoNudityThreshold: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const DEFAULT_MODERATION_CONFIG: ModerationConfig = {
|
|
17
|
-
enabled: true,
|
|
18
|
-
auditLoggingEnabled: true,
|
|
19
|
-
auditLoggingStorePath: './data/moderation-logs',
|
|
20
|
-
sightEngineApiUser: '',
|
|
21
|
-
sightEngineApiSecret: '',
|
|
22
|
-
imagesEnabled: true,
|
|
23
|
-
textEnabled: true,
|
|
24
|
-
videoEnabled: true,
|
|
25
|
-
imageNudityThreshold: 0.5,
|
|
26
|
-
textSexualThreshold: 0.5,
|
|
27
|
-
textToxicThreshold: 0.5,
|
|
28
|
-
videoNudityThreshold: 0.5,
|
|
29
|
-
};
|
package/src/ModerationMixin.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream';
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
3
|
-
import type { Operation } from '@solid/community-server';
|
|
4
|
-
import { getLoggerFor, ForbiddenHttpError, guardStream } from '@solid/community-server';
|
|
5
|
-
import type { ModerationConfig } from './ModerationConfig';
|
|
6
|
-
import { SightEngineProvider } from './providers/SightEngineProvider';
|
|
7
|
-
import { ModerationStore } from './ModerationStore';
|
|
8
|
-
|
|
9
|
-
export class ModerationMixin {
|
|
10
|
-
protected readonly logger = getLoggerFor(this);
|
|
11
|
-
private readonly moderationConfig: ModerationConfig;
|
|
12
|
-
private readonly moderationStore?: ModerationStore;
|
|
13
|
-
|
|
14
|
-
public constructor(moderationConfig: ModerationConfig) {
|
|
15
|
-
this.moderationConfig = moderationConfig;
|
|
16
|
-
this.moderationStore = moderationConfig.auditLoggingEnabled ?
|
|
17
|
-
new ModerationStore(moderationConfig.auditLoggingStorePath) :
|
|
18
|
-
undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
public async moderateContent(operation: Operation): Promise<void> {
|
|
22
|
-
if (!this.moderationConfig.enabled || !operation.body?.data) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const contentType = operation.body.metadata.contentType;
|
|
27
|
-
if (contentType?.startsWith('image/') && this.moderationConfig.imagesEnabled) {
|
|
28
|
-
await this.moderateImageContent(operation);
|
|
29
|
-
} else if (contentType?.startsWith('text/') && this.moderationConfig.textEnabled) {
|
|
30
|
-
await this.moderateTextContent(operation);
|
|
31
|
-
} else if (contentType?.startsWith('video/') && this.moderationConfig.videoEnabled) {
|
|
32
|
-
await this.moderateVideoContent(operation);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
private async moderateImageContent(operation: Operation): Promise<void> {
|
|
37
|
-
const chunks: Buffer[] = [];
|
|
38
|
-
for await (const chunk of operation.body.data) {
|
|
39
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array));
|
|
40
|
-
}
|
|
41
|
-
const buffer = Buffer.concat(chunks);
|
|
42
|
-
operation.body.data = guardStream(Readable.from(buffer));
|
|
43
|
-
|
|
44
|
-
const tempFile = `/tmp/moderation_${Date.now()}.jpg`;
|
|
45
|
-
await fs.writeFile(tempFile, buffer);
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const client = new SightEngineProvider(
|
|
49
|
-
this.moderationConfig.sightEngineApiUser,
|
|
50
|
-
this.moderationConfig.sightEngineApiSecret,
|
|
51
|
-
);
|
|
52
|
-
const result = await client.analyzeImage(tempFile);
|
|
53
|
-
this.logger.info(`MODERATION: Image analysis result - nudity score: ${result.nudity?.raw}, threshold: ${this.moderationConfig.imageNudityThreshold}`);
|
|
54
|
-
|
|
55
|
-
if (result.nudity?.raw && result.nudity.raw > this.moderationConfig.imageNudityThreshold) {
|
|
56
|
-
if (this.moderationStore) {
|
|
57
|
-
await this.moderationStore.recordViolation({
|
|
58
|
-
contentType: 'image',
|
|
59
|
-
resourcePath: operation.target.path,
|
|
60
|
-
violations: [{ model: 'nudity', score: result.nudity.raw, threshold: this.moderationConfig.imageNudityThreshold }],
|
|
61
|
-
contentSize: buffer.length,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
throw new ForbiddenHttpError('Upload blocked: Content violates community guidelines');
|
|
65
|
-
}
|
|
66
|
-
} catch (error: unknown) {
|
|
67
|
-
if (error instanceof ForbiddenHttpError) {
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
this.logger.warn(`MODERATION: Image check failed (fail-open): ${error}`);
|
|
71
|
-
} finally {
|
|
72
|
-
await fs.unlink(tempFile).catch(() => {});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private async moderateTextContent(operation: Operation): Promise<void> {
|
|
77
|
-
const chunks: Buffer[] = [];
|
|
78
|
-
for await (const chunk of operation.body.data) {
|
|
79
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array));
|
|
80
|
-
}
|
|
81
|
-
const buffer = Buffer.concat(chunks);
|
|
82
|
-
const text = buffer.toString('utf8');
|
|
83
|
-
operation.body.data = guardStream(Readable.from(buffer));
|
|
84
|
-
|
|
85
|
-
if (!text?.trim()) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const client = new SightEngineProvider(
|
|
91
|
-
this.moderationConfig.sightEngineApiUser,
|
|
92
|
-
this.moderationConfig.sightEngineApiSecret,
|
|
93
|
-
);
|
|
94
|
-
const result = await client.analyzeText(text);
|
|
95
|
-
|
|
96
|
-
if (result.sexual > this.moderationConfig.textSexualThreshold || result.toxic > this.moderationConfig.textToxicThreshold) {
|
|
97
|
-
if (this.moderationStore) {
|
|
98
|
-
await this.moderationStore.recordViolation({
|
|
99
|
-
contentType: 'text',
|
|
100
|
-
resourcePath: operation.target.path,
|
|
101
|
-
violations: [{ model: 'text', score: Math.max(result.sexual, result.toxic), threshold: Math.min(this.moderationConfig.textSexualThreshold, this.moderationConfig.textToxicThreshold) }],
|
|
102
|
-
contentSize: buffer.length,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
throw new ForbiddenHttpError('Upload blocked: Content violates community guidelines');
|
|
106
|
-
}
|
|
107
|
-
} catch (error: unknown) {
|
|
108
|
-
if (error instanceof ForbiddenHttpError) {
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
this.logger.warn(`MODERATION: Text check failed (fail-open): ${error}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private async moderateVideoContent(operation: Operation): Promise<void> {
|
|
116
|
-
const chunks: Buffer[] = [];
|
|
117
|
-
for await (const chunk of operation.body.data) {
|
|
118
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array));
|
|
119
|
-
}
|
|
120
|
-
const buffer = Buffer.concat(chunks);
|
|
121
|
-
operation.body.data = guardStream(Readable.from(buffer));
|
|
122
|
-
|
|
123
|
-
const tempFile = `/tmp/moderation_${Date.now()}.mp4`;
|
|
124
|
-
await fs.writeFile(tempFile, buffer);
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const client = new SightEngineProvider(
|
|
128
|
-
this.moderationConfig.sightEngineApiUser,
|
|
129
|
-
this.moderationConfig.sightEngineApiSecret,
|
|
130
|
-
);
|
|
131
|
-
const result = await client.analyzeVideo(tempFile);
|
|
132
|
-
|
|
133
|
-
if (result.nudity?.raw && result.nudity.raw > this.moderationConfig.videoNudityThreshold) {
|
|
134
|
-
if (this.moderationStore) {
|
|
135
|
-
await this.moderationStore.recordViolation({
|
|
136
|
-
contentType: 'video',
|
|
137
|
-
resourcePath: operation.target.path,
|
|
138
|
-
violations: [{ model: 'nudity', score: result.nudity.raw, threshold: this.moderationConfig.videoNudityThreshold }],
|
|
139
|
-
contentSize: buffer.length,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
throw new ForbiddenHttpError('Upload blocked: Content violates community guidelines');
|
|
143
|
-
}
|
|
144
|
-
} catch (error: unknown) {
|
|
145
|
-
if (error instanceof ForbiddenHttpError) {
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
this.logger.warn(`MODERATION: Video check failed (fail-open): ${error}`);
|
|
149
|
-
} finally {
|
|
150
|
-
await fs.unlink(tempFile).catch(() => {});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { getLoggerFor } from '@solid/community-server';
|
|
2
|
-
import type { ModerationConfig } from './ModerationConfig';
|
|
3
|
-
import type { ResponseDescription, OperationHandlerInput, OperationHandler } from '@solid/community-server';
|
|
4
|
-
import { ModerationMixin } from './ModerationMixin';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Wraps an OperationHandler to add content moderation for operations with body data.
|
|
8
|
-
* Checks PUT, POST, and PATCH operations before passing them to the wrapped handler.
|
|
9
|
-
*
|
|
10
|
-
* @module
|
|
11
|
-
*/
|
|
12
|
-
export class ModerationOperationHandler {
|
|
13
|
-
protected readonly logger = getLoggerFor(this);
|
|
14
|
-
private readonly source: OperationHandler;
|
|
15
|
-
private readonly moderationMixin: ModerationMixin;
|
|
16
|
-
|
|
17
|
-
public constructor(
|
|
18
|
-
source: OperationHandler,
|
|
19
|
-
enabled: boolean,
|
|
20
|
-
auditLoggingEnabled: boolean,
|
|
21
|
-
auditLoggingStorePath: string,
|
|
22
|
-
sightEngineApiUser: string,
|
|
23
|
-
sightEngineApiSecret: string,
|
|
24
|
-
imagesEnabled: boolean,
|
|
25
|
-
textEnabled: boolean,
|
|
26
|
-
videoEnabled: boolean,
|
|
27
|
-
imageNudityThreshold: number,
|
|
28
|
-
textSexualThreshold: number,
|
|
29
|
-
textToxicThreshold: number,
|
|
30
|
-
videoNudityThreshold: number,
|
|
31
|
-
) {
|
|
32
|
-
this.source = source;
|
|
33
|
-
this.moderationMixin = new ModerationMixin({
|
|
34
|
-
enabled,
|
|
35
|
-
auditLoggingEnabled,
|
|
36
|
-
auditLoggingStorePath,
|
|
37
|
-
sightEngineApiUser,
|
|
38
|
-
sightEngineApiSecret,
|
|
39
|
-
imagesEnabled,
|
|
40
|
-
textEnabled,
|
|
41
|
-
videoEnabled,
|
|
42
|
-
imageNudityThreshold,
|
|
43
|
-
textSexualThreshold,
|
|
44
|
-
textToxicThreshold,
|
|
45
|
-
videoNudityThreshold,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
public async canHandle(input: OperationHandlerInput): Promise<void> {
|
|
50
|
-
return this.source.canHandle(input);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public async handle(input: OperationHandlerInput): Promise<ResponseDescription> {
|
|
54
|
-
const { operation } = input;
|
|
55
|
-
|
|
56
|
-
// Only moderate operations with body data
|
|
57
|
-
if (operation.body?.data && [ 'PUT', 'POST', 'PATCH' ].includes(operation.method)) {
|
|
58
|
-
this.logger.info(`MODERATION: Checking ${operation.method} request to ${operation.target.path}`);
|
|
59
|
-
await this.moderationMixin.moderateContent(operation);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return this.source.handle(input);
|
|
63
|
-
}
|
|
64
|
-
}
|
package/src/ModerationRecord.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Record of a content moderation violation for admin review.
|
|
3
|
-
*/
|
|
4
|
-
export interface ModerationRecord {
|
|
5
|
-
id: string;
|
|
6
|
-
timestamp: Date;
|
|
7
|
-
contentType: 'image' | 'text' | 'video';
|
|
8
|
-
resourcePath: string;
|
|
9
|
-
userWebId?: string;
|
|
10
|
-
userAgent?: string;
|
|
11
|
-
clientIp?: string;
|
|
12
|
-
violations: {
|
|
13
|
-
model: string;
|
|
14
|
-
score: number;
|
|
15
|
-
threshold: number;
|
|
16
|
-
}[];
|
|
17
|
-
contentSize: number;
|
|
18
|
-
contentHash?: string;
|
|
19
|
-
}
|