@nostrify/policies 0.36.6 → 0.36.8
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/.turbo/turbo-build.log +19 -28
- package/.turbo/turbo-test.log +53 -4
- package/CHANGELOG.md +18 -0
- package/dist/AntiDuplicationPolicy.ts +82 -0
- package/dist/AnyPolicy.ts +24 -0
- package/dist/AuthorPolicy.ts +29 -0
- package/dist/DomainPolicy.ts +96 -0
- package/dist/FiltersPolicy.ts +30 -0
- package/dist/HashtagPolicy.ts +28 -0
- package/dist/HellthreadPolicy.ts +30 -0
- package/dist/InvertPolicy.ts +23 -0
- package/dist/KeywordPolicy.ts +28 -0
- package/dist/NoOpPolicy.ts +9 -0
- package/dist/OpenAIPolicy.ts +116 -0
- package/dist/PipePolicy.ts +39 -0
- package/dist/PowPolicy.ts +44 -0
- package/dist/PubkeyBanPolicy.ts +27 -0
- package/dist/ReadOnlyPolicy.ts +9 -0
- package/dist/RegexPolicy.ts +25 -0
- package/dist/ReplyBotPolicy.ts +62 -0
- package/dist/SizePolicy.ts +39 -0
- package/dist/WhitelistPolicy.ts +36 -0
- package/dist/WoTPolicy.ts +64 -0
- package/dist/mod.ts +20 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/dist/mod.js.map +0 -7
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
dist/FiltersPolicy.js 410b
|
|
21
|
-
dist/InvertPolicy.js 385b
|
|
22
|
-
dist/PipePolicy.js 383b
|
|
23
|
-
dist/PubkeyBanPolicy.js 367b
|
|
24
|
-
dist/RegexPolicy.js 332b
|
|
25
|
-
dist/ReadOnlyPolicy.js 187b
|
|
26
|
-
...and 1 more output file...
|
|
27
|
-
|
|
28
|
-
⚡ Done in 51ms
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @nostrify/policies@0.36.7 build /home/sid/repos/nostrify/packages/policies
|
|
4
|
+
> npx tsc -p tsconfig.json && node ../../esbuild.config.js --package ./
|
|
5
|
+
|
|
6
|
+
[1mnpm[22m [33mwarn[39m [94mUnknown env config "verify-deps-before-run". This will stop working in the next major version of npm.[39m
|
|
7
|
+
[1mnpm[22m [33mwarn[39m [94mUnknown env config "_jsr-registry". This will stop working in the next major version of npm.[39m
|
|
8
|
+
[1G[0K⠙[1G[0K[1G[0K⠙[1G[0KBuilding with esbuild...
|
|
9
|
+
|
|
10
|
+
[37mdist/[0m[1mDomainPolicy.js[0m [36m2.1kb[0m
|
|
11
|
+
[37mdist/[0m[1mReplyBotPolicy.js[0m [36m1.4kb[0m
|
|
12
|
+
[37mdist/[0m[1mmod.js[0m [36m1.3kb[0m
|
|
13
|
+
[37mdist/[0m[1mAntiDuplicationPolicy.js[0m [36m1.2kb[0m
|
|
14
|
+
[37mdist/[0m[1mWoTPolicy.js[0m [36m1.0kb[0m
|
|
15
|
+
[37m...and 16 more output files...[0m
|
|
16
|
+
|
|
17
|
+
⚡ [32mDone in 22ms[0m
|
|
18
|
+
Copying source files...
|
|
19
|
+
Done!
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,4 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @nostrify/policies@0.36.6 test /home/sid/repos/nostrify/packages/policies
|
|
4
|
+
> node --test --test-concurrency=1 "**/*.test.ts"
|
|
5
|
+
|
|
6
|
+
✔ AntiDuplicationPolicy (319.690432ms)
|
|
7
|
+
✔ AntiDuplicationPolicy with deobfuscate (44.807015ms)
|
|
8
|
+
✔ accepts when all policies accept (64.674178ms)
|
|
9
|
+
✔ accepts when some policies reject (8.399958ms)
|
|
10
|
+
✔ rejects when all policies reject (5.373917ms)
|
|
11
|
+
✔ AuthorPolicy (48.081135ms)
|
|
12
|
+
✔ DomainPolicy allows events from authors with a valid nip05 (46.436288ms)
|
|
13
|
+
✔ DomainPolicy rejects events from authors without a kind 0 (4.13101ms)
|
|
14
|
+
✔ DomainPolicy rejects events from authors with a missing nip05 (9.361192ms)
|
|
15
|
+
✔ DomainPolicy rejects events from authors with a malformed nip05 (11.704458ms)
|
|
16
|
+
✔ DomainPolicy rejects events from authors with an invalid nip05 (9.725418ms)
|
|
17
|
+
✔ DomainPolicy rejects events from authors with a blacklisted nip05 domain (12.135733ms)
|
|
18
|
+
✔ DomainPolicy rejects events from authors who aren't on a whitelisted domain (8.181353ms)
|
|
19
|
+
✔ DomainPolicy allows events from authors who are on a whitelisted domain (9.511213ms)
|
|
20
|
+
✔ DomainPolicy rejects events from authors with a subdomain of a blacklisted domain (8.78339ms)
|
|
21
|
+
✔ DomainPolicy rejects events from authors with a deeply nested subdomain of a blacklisted domain (8.255036ms)
|
|
22
|
+
✔ DomainPolicy allows events from authors with similar but not subdomain of blacklisted domain (8.391158ms)
|
|
23
|
+
✔ FiltersPolicy (47.201126ms)
|
|
24
|
+
✔ HashtagPolicy (57.834978ms)
|
|
25
|
+
✔ HellthreadPolicy (47.231857ms)
|
|
26
|
+
✔ InvertPolicy (39.831057ms)
|
|
27
|
+
✔ KeywordPolicy (52.182532ms)
|
|
28
|
+
✔ NoOpPolicy (38.95077ms)
|
|
29
|
+
✔ rejects flagged events (95.404554ms)
|
|
30
|
+
✔ accepts unflagged events (58.128733ms)
|
|
31
|
+
✔ passes events through multiple policies (38.444137ms)
|
|
32
|
+
✔ short-circuits on the first reject (5.71684ms)
|
|
33
|
+
✔ accepts when all policies accept (4.445368ms)
|
|
34
|
+
✔ blocks events without a nonce (37.656948ms)
|
|
35
|
+
✔ accepts event with sufficient POW (0.213227ms)
|
|
36
|
+
✔ PubkeyBanPolicy (52.882068ms)
|
|
37
|
+
✔ ReadOnlyPolicy (37.273237ms)
|
|
38
|
+
✔ RegexPolicy (42.23802ms)
|
|
39
|
+
✔ ReplyBotPolicy blocks replies within the same second (44.754564ms)
|
|
40
|
+
✔ ReplyBotPolicy allows replies after 1 second (9.42384ms)
|
|
41
|
+
✔ ReplyBotPolicy allows replies within the same second from users who are tagged (11.768783ms)
|
|
42
|
+
✔ SizePolicy (47.893399ms)
|
|
43
|
+
✔ WhitelistPolicy (48.551241ms)
|
|
44
|
+
✔ WoTPolicy (74.334084ms)
|
|
45
|
+
✔ WoTPolicy constructor with error store (0.898097ms)
|
|
46
|
+
ℹ tests 40
|
|
47
|
+
ℹ suites 0
|
|
48
|
+
ℹ pass 40
|
|
49
|
+
ℹ fail 0
|
|
50
|
+
ℹ cancelled 0
|
|
51
|
+
ℹ skipped 0
|
|
52
|
+
ℹ todo 0
|
|
53
|
+
ℹ duration_ms 5386.054741
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.36.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- distribute ts files
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @nostrify/nostrify@0.46.10
|
|
10
|
+
- @nostrify/types@0.36.6
|
|
11
|
+
|
|
12
|
+
## 0.36.7
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- tests should pass now
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @nostrify/nostrify@0.46.9
|
|
19
|
+
- @nostrify/types@0.36.5
|
|
20
|
+
|
|
3
21
|
## 0.36.6
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
import type { Kv, KvKey } from '@deno/kv';
|
|
3
|
+
|
|
4
|
+
/** Policy options for `AntiDuplicationPolicy`. */
|
|
5
|
+
interface AntiDuplicationPolicyOpts {
|
|
6
|
+
/** Deno.Kv implementation to use. */
|
|
7
|
+
kv: Pick<Kv, 'get' | 'set'>;
|
|
8
|
+
/** Time in ms until a message with this content may be posted again. Default: `60000` (1 minute). */
|
|
9
|
+
expireIn?: number;
|
|
10
|
+
/** Note text under this limit will be skipped by the policy. Default: `50`. */
|
|
11
|
+
minLength?: number;
|
|
12
|
+
/** Normalize the event's content before a hash is taken, to prevent the attacker from making small changes. Should return the normalized content. */
|
|
13
|
+
deobfuscate?(event: NostrEvent): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Prevent messages with the exact same content from being submitted repeatedly.
|
|
18
|
+
*
|
|
19
|
+
* It stores a hashcode for each content in a Deno.Kv database and rate-limits them. Only messages that meet the minimum length criteria are selected.
|
|
20
|
+
* Each time a matching message is submitted, the timer will reset, so spammers sending the same message will only ever get the first one through.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* // Open a Deno.KV instance.
|
|
24
|
+
* const kv = await Deno.openKv();
|
|
25
|
+
*
|
|
26
|
+
* // Prevent the same message from being posted within 60 seconds.
|
|
27
|
+
* new AntiDuplicationPolicy({ kv, expireIn: 60000 });
|
|
28
|
+
*
|
|
29
|
+
* // Only enforce the policy on messages with at least 50 characters.
|
|
30
|
+
* new AntiDuplicationPolicy({ kv, expireIn: 60000, minLength: 50 });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class AntiDuplicationPolicy implements NPolicy {
|
|
34
|
+
private opts: AntiDuplicationPolicyOpts;
|
|
35
|
+
|
|
36
|
+
constructor(opts: AntiDuplicationPolicyOpts) {
|
|
37
|
+
this.opts = opts;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
41
|
+
const { id, kind } = event;
|
|
42
|
+
const { kv, expireIn = 60000, minLength = 50, deobfuscate } = this.opts;
|
|
43
|
+
|
|
44
|
+
const content = deobfuscate?.(event) ?? event.content;
|
|
45
|
+
|
|
46
|
+
if (kind === 1 && content.length >= minLength) {
|
|
47
|
+
const hash = AntiDuplicationPolicy.hashCode(content);
|
|
48
|
+
const key: KvKey = ['nostrify', 'policies', 'antiduplication', hash];
|
|
49
|
+
|
|
50
|
+
const { value } = await kv.get(key);
|
|
51
|
+
|
|
52
|
+
if (value) {
|
|
53
|
+
await kv.set(key, true, { expireIn });
|
|
54
|
+
return [
|
|
55
|
+
'OK',
|
|
56
|
+
id,
|
|
57
|
+
false,
|
|
58
|
+
'blocked: the same message has been repeated too many times',
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await kv.set(key, true, { expireIn });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return ['OK', id, true, ''];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get a "good enough" unique identifier for this content.
|
|
70
|
+
* This algorithm was chosen because it's very fast with a low chance of collisions.
|
|
71
|
+
* https://stackoverflow.com/a/8831937
|
|
72
|
+
*/
|
|
73
|
+
private static hashCode(str: string): number {
|
|
74
|
+
let hash = 0;
|
|
75
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
76
|
+
const chr = str.charCodeAt(i);
|
|
77
|
+
hash = (hash << 5) - hash + chr;
|
|
78
|
+
hash |= 0; // Convert to 32bit integer
|
|
79
|
+
}
|
|
80
|
+
return hash;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** Similar to `PipePolicy`, but passes if at least one policy passes. */
|
|
4
|
+
export class AnyPolicy implements NPolicy {
|
|
5
|
+
private policies: NPolicy[];
|
|
6
|
+
constructor(policies: NPolicy[]) {
|
|
7
|
+
this.policies = policies;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK> {
|
|
11
|
+
let result: NostrRelayOK = ['OK', event.id, false, 'blocked: no policy passed'];
|
|
12
|
+
|
|
13
|
+
for (const policy of this.policies) {
|
|
14
|
+
result = await policy.call(event, signal);
|
|
15
|
+
|
|
16
|
+
const ok = result[2];
|
|
17
|
+
if (ok) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy, NStore } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** Rejects events by authors without a kind 0, then optionally applies another policy to the kind 0. */
|
|
4
|
+
export class AuthorPolicy implements NPolicy {
|
|
5
|
+
private store: NStore;
|
|
6
|
+
private policy?: NPolicy;
|
|
7
|
+
|
|
8
|
+
constructor(store: NStore, policy?: NPolicy) {
|
|
9
|
+
this.store = store;
|
|
10
|
+
this.policy = policy;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK> {
|
|
14
|
+
const author: NostrEvent | undefined = event.kind === 0 ? event : await this.store
|
|
15
|
+
.query([{ kinds: [0], authors: [event.pubkey], limit: 1 }], { signal })
|
|
16
|
+
.then(([event]: NostrEvent[]) => event);
|
|
17
|
+
|
|
18
|
+
if (!author) {
|
|
19
|
+
return ['OK', event.id, false, 'blocked: author is missing a kind 0 event'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (this.policy) {
|
|
23
|
+
const [, , ok, reason] = await this.policy.call(author, signal);
|
|
24
|
+
return ['OK', event.id, ok, reason];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return ['OK', event.id, true, ''];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { NIP05, NSchema as n } from '@nostrify/nostrify';
|
|
2
|
+
import type { NostrEvent, NPolicy, NProfilePointer, NStore } from '@nostrify/types';
|
|
3
|
+
|
|
4
|
+
import { AuthorPolicy } from './AuthorPolicy.ts';
|
|
5
|
+
|
|
6
|
+
/** Options for `DomainPolicy`. */
|
|
7
|
+
interface DomainPolicyOpts {
|
|
8
|
+
/** Custom NIP-05 lookup function. */
|
|
9
|
+
lookup?(nip05: string, signal?: AbortSignal): Promise<NProfilePointer>;
|
|
10
|
+
/** List of domains to blacklist. Reject events from users with a NIP-05 matching any of these domains. */
|
|
11
|
+
blacklist?: string[];
|
|
12
|
+
/** List of domains to whitelist. If provided, only events from users with a valid NIP-05 on the given domains will be accepted. */
|
|
13
|
+
whitelist?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Ban events unless their author has a valid NIP-05 name. Domains can also be whitelisted or blacklisted. */
|
|
17
|
+
export class DomainPolicy extends AuthorPolicy implements NPolicy {
|
|
18
|
+
constructor(store: NStore, opts: DomainPolicyOpts = {}) {
|
|
19
|
+
super(store, {
|
|
20
|
+
async call(event: NostrEvent, signal: AbortSignal) {
|
|
21
|
+
const { blacklist = [], whitelist, lookup = DomainPolicy.lookup } = opts;
|
|
22
|
+
|
|
23
|
+
const metadata = n.json().pipe(n.metadata()).safeParse(event.content);
|
|
24
|
+
|
|
25
|
+
if (!metadata.success) {
|
|
26
|
+
return ['OK', event.id, false, 'blocked: invalid kind 0 metadata'];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { nip05 } = metadata.data;
|
|
30
|
+
|
|
31
|
+
if (!nip05) {
|
|
32
|
+
return ['OK', event.id, false, 'blocked: missing nip05'];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const domain = nip05.split('@').pop();
|
|
36
|
+
|
|
37
|
+
if (!domain) {
|
|
38
|
+
return ['OK', event.id, false, 'blocked: invalid nip05'];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (DomainPolicy.isDomainBlacklisted(domain, blacklist)) {
|
|
42
|
+
return ['OK', event.id, false, 'blocked: blacklisted nip05 domain'];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const { pubkey } = await lookup(nip05, signal);
|
|
47
|
+
|
|
48
|
+
if (pubkey !== event.pubkey) {
|
|
49
|
+
return ['OK', event.id, false, 'blocked: mismatched nip05 pubkey'];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (whitelist && !whitelist.includes(domain)) {
|
|
53
|
+
return [
|
|
54
|
+
'OK',
|
|
55
|
+
event.id,
|
|
56
|
+
false,
|
|
57
|
+
'blocked: nip05 domain not in whitelist',
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return ['OK', event.id, true, ''];
|
|
62
|
+
} catch {
|
|
63
|
+
return ['OK', event.id, false, 'blocked: failed to lookup nip05'];
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Check if a domain is blacklisted, including subdomains of blacklisted domains. */
|
|
70
|
+
private static isDomainBlacklisted(
|
|
71
|
+
domain: string,
|
|
72
|
+
blacklist: string[],
|
|
73
|
+
): boolean {
|
|
74
|
+
// Check for exact match
|
|
75
|
+
if (blacklist.includes(domain)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if domain is a subdomain of any blacklisted domain
|
|
80
|
+
for (const blacklistedDomain of blacklist) {
|
|
81
|
+
if (domain.endsWith('.' + blacklistedDomain)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Default NIP-05 lookup method if one isn't provided by the caller. */
|
|
90
|
+
private static lookup(
|
|
91
|
+
nip05: string,
|
|
92
|
+
signal?: AbortSignal,
|
|
93
|
+
): Promise<NProfilePointer> {
|
|
94
|
+
return NIP05.lookup(nip05, { signal });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { matchFilters } from 'nostr-tools';
|
|
2
|
+
|
|
3
|
+
import type { NostrEvent, NostrFilter, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reject events which don't match the filters.
|
|
7
|
+
*
|
|
8
|
+
* Only messages which **match** the filters are allowed, and all others are dropped.
|
|
9
|
+
* The filter is a [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) relay filter.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // Only allow kind 1, 3, 5, and 7 events.
|
|
13
|
+
* new FiltersPolicy([{ kinds: [0, 1, 3, 5, 6, 7] }]);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class FiltersPolicy implements NPolicy {
|
|
17
|
+
private filters: NostrFilter[];
|
|
18
|
+
constructor(filters: NostrFilter[]) {
|
|
19
|
+
this.filters = filters;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// deno-lint-ignore require-await
|
|
23
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
24
|
+
if (matchFilters(this.filters, event)) {
|
|
25
|
+
return ['OK', event.id, true, ''];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return ['OK', event.id, false, "blocked: the event doesn't match the allowed filters"];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reject events containing any of the banned hashtags.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // Reject events with banned hashtags.
|
|
9
|
+
* HashtagPolicy(['nsfw']);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export class HashtagPolicy implements NPolicy {
|
|
13
|
+
private hashtags: string[];
|
|
14
|
+
constructor(hashtags: string[]) {
|
|
15
|
+
this.hashtags = hashtags;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// deno-lint-ignore require-await
|
|
19
|
+
async call({ id, tags }: NostrEvent): Promise<NostrRelayOK> {
|
|
20
|
+
for (const [name, value] of tags) {
|
|
21
|
+
if (name === 't' && this.hashtags.includes(value.toLowerCase())) {
|
|
22
|
+
return ['OK', id, false, 'blocked: contains a banned hashtag'];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return ['OK', id, true, ''];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** Policy options for `HellthreadPolicy`. */
|
|
4
|
+
interface HellthreadPolicyOpts {
|
|
5
|
+
/** Total number of "p" tags a kind 1 note may have before it's rejected. Default: `100` */
|
|
6
|
+
limit?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Basic policy to demonstrate how policies work. Accepts all events. */
|
|
10
|
+
export class HellthreadPolicy implements NPolicy {
|
|
11
|
+
private opts: HellthreadPolicyOpts;
|
|
12
|
+
constructor(opts: HellthreadPolicyOpts = {}) {
|
|
13
|
+
this.opts = opts;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// deno-lint-ignore require-await
|
|
17
|
+
async call({ id, kind, tags }: NostrEvent): Promise<NostrRelayOK> {
|
|
18
|
+
const { limit = 100 } = this.opts;
|
|
19
|
+
|
|
20
|
+
if (kind === 1) {
|
|
21
|
+
const p = tags.filter((tag: string[]) => tag[0] === 'p');
|
|
22
|
+
|
|
23
|
+
if (p.length > limit) {
|
|
24
|
+
return ['OK', id, false, `blocked: rejected due to ${p.length} "p" tags (${limit} is the limit).`];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return ['OK', id, true, ''];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** Rejects if the policy passes, passes if the policy rejects. */
|
|
4
|
+
export class InvertPolicy implements NPolicy {
|
|
5
|
+
private policy: NPolicy;
|
|
6
|
+
private reason: string;
|
|
7
|
+
|
|
8
|
+
constructor(policy: NPolicy, reason: string) {
|
|
9
|
+
this.policy = policy;
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
14
|
+
const result = await this.policy.call(event);
|
|
15
|
+
const ok = result[2];
|
|
16
|
+
|
|
17
|
+
if (ok) {
|
|
18
|
+
return ['OK', event.id, false, this.reason];
|
|
19
|
+
} else {
|
|
20
|
+
return ['OK', event.id, true, ''];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reject events containing any of the strings in its content.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // Reject events with bad words.
|
|
9
|
+
* KeywordPolicy(['moo', 'oink', 'honk']);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export class KeywordPolicy implements NPolicy {
|
|
13
|
+
private words: Iterable<string>;
|
|
14
|
+
constructor(words: Iterable<string>) {
|
|
15
|
+
this.words = words;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// deno-lint-ignore require-await
|
|
19
|
+
async call({ id, content }: NostrEvent): Promise<NostrRelayOK> {
|
|
20
|
+
for (const word of this.words) {
|
|
21
|
+
if (content.toLowerCase().includes(word.toLowerCase())) {
|
|
22
|
+
return ['OK', id, false, 'blocked: contains a banned word or phrase'];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return ['OK', id, true, ''];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/** Basic policy to demonstrate how policies work. Accepts all events. */
|
|
4
|
+
export class NoOpPolicy implements NPolicy {
|
|
5
|
+
// deno-lint-ignore require-await
|
|
6
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
7
|
+
return ['OK', event.id, true, ''];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenAI moderation result.
|
|
5
|
+
*
|
|
6
|
+
* https://platform.openai.com/docs/api-reference/moderations/create
|
|
7
|
+
*/
|
|
8
|
+
interface OpenAIModerationResult {
|
|
9
|
+
id: string;
|
|
10
|
+
model: string;
|
|
11
|
+
results: {
|
|
12
|
+
categories: {
|
|
13
|
+
'hate': boolean;
|
|
14
|
+
'hate/threatening': boolean;
|
|
15
|
+
'self-harm': boolean;
|
|
16
|
+
'sexual': boolean;
|
|
17
|
+
'sexual/minors': boolean;
|
|
18
|
+
'violence': boolean;
|
|
19
|
+
'violence/graphic': boolean;
|
|
20
|
+
};
|
|
21
|
+
category_scores: {
|
|
22
|
+
'hate': number;
|
|
23
|
+
'hate/threatening': number;
|
|
24
|
+
'self-harm': number;
|
|
25
|
+
'sexual': number;
|
|
26
|
+
'sexual/minors': number;
|
|
27
|
+
'violence': number;
|
|
28
|
+
'violence/graphic': number;
|
|
29
|
+
};
|
|
30
|
+
flagged: boolean;
|
|
31
|
+
}[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Policy options for `OpenAIPolicy`. */
|
|
35
|
+
interface OpenAIPolicyOpts {
|
|
36
|
+
/**
|
|
37
|
+
* Callback for fine control over the policy. It contains the event and the OpenAI moderation data.
|
|
38
|
+
* Implementations should return `true` to **reject** the content, and `false` to accept.
|
|
39
|
+
*/
|
|
40
|
+
handler?(event: NostrEvent, result: OpenAIModerationResult): boolean;
|
|
41
|
+
/** Custom endpoint to use instead of `https://api.openai.com/v1/moderations`. */
|
|
42
|
+
endpoint?: string;
|
|
43
|
+
/** Custom fetch implementation. */
|
|
44
|
+
fetch?: typeof fetch;
|
|
45
|
+
/** Which event kinds to apply the policy to. */
|
|
46
|
+
kinds?: number[];
|
|
47
|
+
/** OpenAI API key for making the requests. */
|
|
48
|
+
apiKey: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Passes event content to OpenAI and then rejects flagged events.
|
|
53
|
+
*
|
|
54
|
+
* By default, this policy will reject kind 1 events that OpenAI flags.
|
|
55
|
+
* It's possible to pass a custom handler for more control. An OpenAI API key is required.
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* // Default handler. It's so strict it's suitable for children.
|
|
59
|
+
* new OpenAIPolicy({ apiKey: Deno.env.get('OPENAI_API_KEY') });
|
|
60
|
+
*
|
|
61
|
+
* // With a custom handler.
|
|
62
|
+
* new OpenAIPolicy({
|
|
63
|
+
* apiKey: Deno.env.get('OPENAI_API_KEY'),
|
|
64
|
+
* handler(event, data) {
|
|
65
|
+
* // Loop each result.
|
|
66
|
+
* return data.results.some((result) => {
|
|
67
|
+
* if (result.flagged) {
|
|
68
|
+
* const { sexual, violence } = result.categories;
|
|
69
|
+
* // Reject only events flagged as sexual and violent.
|
|
70
|
+
* return sexual && violence;
|
|
71
|
+
* }
|
|
72
|
+
* });
|
|
73
|
+
* },
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export class OpenAIPolicy implements NPolicy {
|
|
78
|
+
private opts: OpenAIPolicyOpts;
|
|
79
|
+
constructor(opts: OpenAIPolicyOpts) {
|
|
80
|
+
this.opts = opts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK> {
|
|
84
|
+
const {
|
|
85
|
+
handler = (_, { results }) => results.some((r) => r.flagged),
|
|
86
|
+
endpoint = 'https://api.openai.com/v1/moderations',
|
|
87
|
+
kinds = [1, 30023],
|
|
88
|
+
apiKey,
|
|
89
|
+
} = this.opts;
|
|
90
|
+
|
|
91
|
+
if (kinds.includes(event.kind)) {
|
|
92
|
+
try {
|
|
93
|
+
const resp = await fetch(endpoint, {
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
97
|
+
},
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
input: event.content,
|
|
100
|
+
}),
|
|
101
|
+
signal,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const result = await resp.json();
|
|
105
|
+
|
|
106
|
+
if (handler(event, result)) {
|
|
107
|
+
return ['OK', event.id, false, 'blocked: content flagged by AI'];
|
|
108
|
+
}
|
|
109
|
+
} catch (_) {
|
|
110
|
+
return ['OK', event.id, false, 'blocked: error analyzing content'];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return ['OK', event.id, true, ''];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Processes events through multiple policies.
|
|
5
|
+
*
|
|
6
|
+
* If any policy rejects, the pipeline will stop and return the rejected message.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const policy = new PipePolicy([
|
|
10
|
+
* new NoOpPolicy(),
|
|
11
|
+
* new FiltersPolicy([{ kinds: [0, 1, 3, 5, 7, 1984, 9734, 9735, 10002] }]),
|
|
12
|
+
* new KeywordPolicy(['https://t.me/']),
|
|
13
|
+
* new RegexPolicy(/(🟠|🔥|😳)ChtaGPT/i),
|
|
14
|
+
* new PubkeyBanPolicy(['e810fafa1e89cdf80cced8e013938e87e21b699b24c8570537be92aec4b12c18']),
|
|
15
|
+
* new HellthreadPolicy({ limit: 100 }),
|
|
16
|
+
* new AntiDuplicationPolicy({ kv: await Deno.openKv(), expireIn: 60000, minLength: 50 }),
|
|
17
|
+
* ]);
|
|
18
|
+
*
|
|
19
|
+
* const [_, eventId, ok, reason] = await policy.call(event);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class PipePolicy implements NPolicy {
|
|
23
|
+
private policies: NPolicy[] = [];
|
|
24
|
+
constructor(policies: NPolicy[]) {
|
|
25
|
+
this.policies = policies;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK> {
|
|
29
|
+
for (const policy of this.policies) {
|
|
30
|
+
const [_, eventId, ok, reason] = await policy.call(event, signal);
|
|
31
|
+
|
|
32
|
+
if (!ok) {
|
|
33
|
+
return [_, eventId, ok, reason];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return ['OK', event.id, true, ''];
|
|
38
|
+
}
|
|
39
|
+
}
|