@nostrify/policies 0.36.2
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/daemon/7bb8240f68f7ad88-turbo.log.2025-07-29 +0 -0
- package/.turbo/turbo-build.log +5 -0
- package/AntiDuplicationPolicy.test.ts +50 -0
- package/AntiDuplicationPolicy.ts +78 -0
- package/AnyPolicy.test.ts +57 -0
- package/AnyPolicy.ts +21 -0
- package/AuthorPolicy.test.ts +23 -0
- package/AuthorPolicy.ts +23 -0
- package/CHANGELOG.md +66 -0
- package/DomainPolicy.test.ts +246 -0
- package/DomainPolicy.ts +96 -0
- package/FiltersPolicy.test.ts +14 -0
- package/FiltersPolicy.ts +27 -0
- package/HashtagPolicy.test.ts +34 -0
- package/HashtagPolicy.ts +25 -0
- package/HellthreadPolicy.test.ts +23 -0
- package/HellthreadPolicy.ts +27 -0
- package/InvertPolicy.test.ts +19 -0
- package/InvertPolicy.ts +17 -0
- package/KeywordPolicy.test.ts +28 -0
- package/KeywordPolicy.ts +25 -0
- package/LICENSE +21 -0
- package/NoOpPolicy.test.ts +18 -0
- package/NoOpPolicy.ts +9 -0
- package/OpenAIPolicy.test.ts +42 -0
- package/OpenAIPolicy.ts +113 -0
- package/PipePolicy.test.ts +58 -0
- package/PipePolicy.ts +36 -0
- package/PowPolicy.test.ts +27 -0
- package/PowPolicy.ts +41 -0
- package/PubkeyBanPolicy.test.ts +17 -0
- package/PubkeyBanPolicy.ts +24 -0
- package/README.md +34 -0
- package/ReadOnlyPolicy.test.ts +19 -0
- package/ReadOnlyPolicy.ts +9 -0
- package/RegexPolicy.test.ts +17 -0
- package/RegexPolicy.ts +22 -0
- package/ReplyBotPolicy.test.ts +50 -0
- package/ReplyBotPolicy.ts +59 -0
- package/SizePolicy.test.ts +21 -0
- package/SizePolicy.ts +35 -0
- package/WhitelistPolicy.test.ts +17 -0
- package/WhitelistPolicy.ts +32 -0
- package/WoTPolicy.test.ts +35 -0
- package/WoTPolicy.ts +61 -0
- package/dist/AntiDuplicationPolicy.d.ts +43 -0
- package/dist/AntiDuplicationPolicy.d.ts.map +1 -0
- package/dist/AntiDuplicationPolicy.js +63 -0
- package/dist/AntiDuplicationPolicy.js.map +1 -0
- package/dist/AnyPolicy.d.ts +8 -0
- package/dist/AnyPolicy.d.ts.map +1 -0
- package/dist/AnyPolicy.js +23 -0
- package/dist/AnyPolicy.js.map +1 -0
- package/dist/AuthorPolicy.d.ts +9 -0
- package/dist/AuthorPolicy.d.ts.map +1 -0
- package/dist/AuthorPolicy.js +27 -0
- package/dist/AuthorPolicy.js.map +1 -0
- package/dist/DomainPolicy.d.ts +21 -0
- package/dist/DomainPolicy.d.ts.map +1 -0
- package/dist/DomainPolicy.js +68 -0
- package/dist/DomainPolicy.js.map +1 -0
- package/dist/FiltersPolicy.d.ts +18 -0
- package/dist/FiltersPolicy.d.ts.map +1 -0
- package/dist/FiltersPolicy.js +30 -0
- package/dist/FiltersPolicy.js.map +1 -0
- package/dist/HashtagPolicy.d.ts +16 -0
- package/dist/HashtagPolicy.d.ts.map +1 -0
- package/dist/HashtagPolicy.js +29 -0
- package/dist/HashtagPolicy.js.map +1 -0
- package/dist/HellthreadPolicy.d.ts +14 -0
- package/dist/HellthreadPolicy.d.ts.map +1 -0
- package/dist/HellthreadPolicy.js +23 -0
- package/dist/HellthreadPolicy.js.map +1 -0
- package/dist/InvertPolicy.d.ts +9 -0
- package/dist/InvertPolicy.d.ts.map +1 -0
- package/dist/InvertPolicy.js +24 -0
- package/dist/InvertPolicy.js.map +1 -0
- package/dist/KeywordPolicy.d.ts +16 -0
- package/dist/KeywordPolicy.d.ts.map +1 -0
- package/dist/KeywordPolicy.js +29 -0
- package/dist/KeywordPolicy.js.map +1 -0
- package/dist/NoOpPolicy.d.ts +6 -0
- package/dist/NoOpPolicy.d.ts.map +1 -0
- package/dist/NoOpPolicy.js +12 -0
- package/dist/NoOpPolicy.js.map +1 -0
- package/dist/OpenAIPolicy.d.ts +80 -0
- package/dist/OpenAIPolicy.d.ts.map +1 -0
- package/dist/OpenAIPolicy.js +62 -0
- package/dist/OpenAIPolicy.js.map +1 -0
- package/dist/PipePolicy.d.ts +26 -0
- package/dist/PipePolicy.d.ts.map +1 -0
- package/dist/PipePolicy.js +39 -0
- package/dist/PipePolicy.js.map +1 -0
- package/dist/PowPolicy.d.ts +21 -0
- package/dist/PowPolicy.d.ts.map +1 -0
- package/dist/PowPolicy.js +36 -0
- package/dist/PowPolicy.js.map +1 -0
- package/dist/PubkeyBanPolicy.d.ts +15 -0
- package/dist/PubkeyBanPolicy.d.ts.map +1 -0
- package/dist/PubkeyBanPolicy.js +28 -0
- package/dist/PubkeyBanPolicy.js.map +1 -0
- package/dist/ReadOnlyPolicy.d.ts +6 -0
- package/dist/ReadOnlyPolicy.d.ts.map +1 -0
- package/dist/ReadOnlyPolicy.js +12 -0
- package/dist/ReadOnlyPolicy.js.map +1 -0
- package/dist/RegexPolicy.d.ts +15 -0
- package/dist/RegexPolicy.d.ts.map +1 -0
- package/dist/RegexPolicy.js +26 -0
- package/dist/RegexPolicy.js.map +1 -0
- package/dist/ReplyBotPolicy.d.ts +25 -0
- package/dist/ReplyBotPolicy.d.ts.map +1 -0
- package/dist/ReplyBotPolicy.js +45 -0
- package/dist/ReplyBotPolicy.js.map +1 -0
- package/dist/SizePolicy.d.ts +23 -0
- package/dist/SizePolicy.d.ts.map +1 -0
- package/dist/SizePolicy.js +31 -0
- package/dist/SizePolicy.js.map +1 -0
- package/dist/WhitelistPolicy.d.ts +16 -0
- package/dist/WhitelistPolicy.d.ts.map +1 -0
- package/dist/WhitelistPolicy.js +35 -0
- package/dist/WhitelistPolicy.js.map +1 -0
- package/dist/WoTPolicy.d.ts +26 -0
- package/dist/WoTPolicy.d.ts.map +1 -0
- package/dist/WoTPolicy.js +42 -0
- package/dist/WoTPolicy.js.map +1 -0
- package/dist/mod.d.ts +21 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +44 -0
- package/dist/mod.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/mod.ts +20 -0
- package/package.json +20 -0
- package/tsconfig.json +14 -0
package/FiltersPolicy.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { matchFilters } from 'nostr-tools';
|
|
2
|
+
|
|
3
|
+
import { 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
|
+
constructor(private filters: NostrFilter[]) {}
|
|
18
|
+
|
|
19
|
+
// deno-lint-ignore require-await
|
|
20
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
21
|
+
if (matchFilters(this.filters, event)) {
|
|
22
|
+
return ['OK', event.id, true, ''];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return ['OK', event.id, false, "blocked: the event doesn't match the allowed filters"];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { HashtagPolicy } from './HashtagPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('HashtagPolicy', async () => {
|
|
7
|
+
const hashtags = ['nsfw'];
|
|
8
|
+
|
|
9
|
+
const event1 = finalizeEvent(
|
|
10
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
11
|
+
generateSecretKey(),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const event2 = finalizeEvent(
|
|
15
|
+
{ kind: 1, content: '', tags: [['t', 'nsfw'], ['t', 'other']], created_at: 0 },
|
|
16
|
+
generateSecretKey(),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const event3 = finalizeEvent(
|
|
20
|
+
{ kind: 1, content: 'nsfw', tags: [], created_at: 0 },
|
|
21
|
+
generateSecretKey(),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const event4 = finalizeEvent(
|
|
25
|
+
{ kind: 1, content: '', tags: [['p', 'nsfw'], ['t', 'other']], created_at: 0 },
|
|
26
|
+
generateSecretKey(),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
assertEquals((await new HashtagPolicy(hashtags).call(event1))[2], true);
|
|
30
|
+
assertEquals((await new HashtagPolicy(hashtags).call(event2))[2], false);
|
|
31
|
+
assertEquals((await new HashtagPolicy([]).call(event2))[2], true);
|
|
32
|
+
assertEquals((await new HashtagPolicy(hashtags).call(event3))[2], true);
|
|
33
|
+
assertEquals((await new HashtagPolicy(hashtags).call(event4))[2], true);
|
|
34
|
+
});
|
package/HashtagPolicy.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { 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
|
+
constructor(private hashtags: string[]) {}
|
|
14
|
+
|
|
15
|
+
// deno-lint-ignore require-await
|
|
16
|
+
async call({ id, tags }: NostrEvent): Promise<NostrRelayOK> {
|
|
17
|
+
for (const [name, value] of tags) {
|
|
18
|
+
if (name === 't' && this.hashtags.includes(value.toLowerCase())) {
|
|
19
|
+
return ['OK', id, false, 'blocked: contains a banned hashtag'];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return ['OK', id, true, ''];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { HellthreadPolicy } from './HellthreadPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('HellthreadPolicy', async () => {
|
|
7
|
+
const policy = new HellthreadPolicy({ limit: 1 });
|
|
8
|
+
|
|
9
|
+
const okEvent = finalizeEvent(
|
|
10
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
11
|
+
generateSecretKey(),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const badEvent = finalizeEvent({
|
|
15
|
+
kind: 1,
|
|
16
|
+
content: '',
|
|
17
|
+
tags: [['p'], ['p'], ['p']],
|
|
18
|
+
created_at: 0,
|
|
19
|
+
}, generateSecretKey());
|
|
20
|
+
|
|
21
|
+
assertEquals((await policy.call(okEvent))[2], true);
|
|
22
|
+
assertEquals((await policy.call(badEvent))[2], false);
|
|
23
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { 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
|
+
constructor(private opts: HellthreadPolicyOpts = {}) {}
|
|
12
|
+
|
|
13
|
+
// deno-lint-ignore require-await
|
|
14
|
+
async call({ id, kind, tags }: NostrEvent): Promise<NostrRelayOK> {
|
|
15
|
+
const { limit = 100 } = this.opts;
|
|
16
|
+
|
|
17
|
+
if (kind === 1) {
|
|
18
|
+
const p = tags.filter((tag) => tag[0] === 'p');
|
|
19
|
+
|
|
20
|
+
if (p.length > limit) {
|
|
21
|
+
return ['OK', id, false, `blocked: rejected due to ${p.length} "p" tags (${limit} is the limit).`];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return ['OK', id, true, ''];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { InvertPolicy } from './InvertPolicy.ts';
|
|
5
|
+
import { NoOpPolicy } from './NoOpPolicy.ts';
|
|
6
|
+
|
|
7
|
+
Deno.test('InvertPolicy', async () => {
|
|
8
|
+
const policy = new InvertPolicy(new NoOpPolicy(), 'blocked: inverted');
|
|
9
|
+
|
|
10
|
+
const event = finalizeEvent(
|
|
11
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
12
|
+
generateSecretKey(),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const [_, _eventId, ok, reason] = await policy.call(event);
|
|
16
|
+
|
|
17
|
+
assertEquals(ok, false);
|
|
18
|
+
assertEquals(reason, 'blocked: inverted');
|
|
19
|
+
});
|
package/InvertPolicy.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { 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
|
+
constructor(private policy: NPolicy, private reason: string) {}
|
|
6
|
+
|
|
7
|
+
async call(event: NostrEvent): Promise<NostrRelayOK> {
|
|
8
|
+
const result = await this.policy.call(event);
|
|
9
|
+
const ok = result[2];
|
|
10
|
+
|
|
11
|
+
if (ok) {
|
|
12
|
+
return ['OK', event.id, false, this.reason];
|
|
13
|
+
} else {
|
|
14
|
+
return ['OK', event.id, true, ''];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { KeywordPolicy } from './KeywordPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('KeywordPolicy', async () => {
|
|
7
|
+
const words = ['https://t.me/spam', 'hello world'];
|
|
8
|
+
|
|
9
|
+
const event1 = finalizeEvent(
|
|
10
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
11
|
+
generateSecretKey(),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const event2 = finalizeEvent(
|
|
15
|
+
{ kind: 1, content: '🔥🔥🔥 https://t.me/spam 我想æ»', tags: [], created_at: 0 },
|
|
16
|
+
generateSecretKey(),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const event3 = finalizeEvent(
|
|
20
|
+
{ kind: 1, content: 'hElLo wOrLd!', tags: [], created_at: 0 },
|
|
21
|
+
generateSecretKey(),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
assertEquals((await new KeywordPolicy(words).call(event1))[2], true);
|
|
25
|
+
assertEquals((await new KeywordPolicy(words).call(event2))[2], false);
|
|
26
|
+
assertEquals((await new KeywordPolicy([]).call(event2))[2], true);
|
|
27
|
+
assertEquals((await new KeywordPolicy(words).call(event3))[2], false);
|
|
28
|
+
});
|
package/KeywordPolicy.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { 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
|
+
constructor(private words: Iterable<string>) {}
|
|
14
|
+
|
|
15
|
+
// deno-lint-ignore require-await
|
|
16
|
+
async call({ id, content }: NostrEvent): Promise<NostrRelayOK> {
|
|
17
|
+
for (const word of this.words) {
|
|
18
|
+
if (content.toLowerCase().includes(word.toLowerCase())) {
|
|
19
|
+
return ['OK', id, false, 'blocked: contains a banned word or phrase'];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return ['OK', id, true, ''];
|
|
24
|
+
}
|
|
25
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Alex Gleason
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { NoOpPolicy } from './NoOpPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('NoOpPolicy', async () => {
|
|
7
|
+
const policy = new NoOpPolicy();
|
|
8
|
+
|
|
9
|
+
const event = finalizeEvent(
|
|
10
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
11
|
+
generateSecretKey(),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const [_, eventId, ok] = await policy.call(event);
|
|
15
|
+
|
|
16
|
+
assertEquals(eventId, event.id);
|
|
17
|
+
assertEquals(ok, true);
|
|
18
|
+
});
|
package/NoOpPolicy.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { 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,42 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { OpenAIPolicy } from './OpenAIPolicy.ts';
|
|
5
|
+
|
|
6
|
+
const timeout = 50;
|
|
7
|
+
|
|
8
|
+
Deno.test('rejects flagged events', async () => {
|
|
9
|
+
const event = finalizeEvent(
|
|
10
|
+
{ kind: 1, content: 'I want to kill them.', tags: [], created_at: 0 },
|
|
11
|
+
generateSecretKey(),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const fetch = () =>
|
|
15
|
+
Promise.resolve(
|
|
16
|
+
new Response(
|
|
17
|
+
'{"id":"modr-6zvK0JiWLBpJvA5IrJufw8BHPpEpB","model":"text-moderation-004","results":[{"flagged":true,"categories":{"sexual":false,"hate":false,"violence":true,"self-harm":false,"sexual/minors":false,"hate/threatening":false,"violence/graphic":false},"category_scores":{"sexual":9.759669410414062e-07,"hate":0.180674210190773,"violence":0.8864424824714661,"self-harm":1.8088556208439854e-09,"sexual/minors":1.3363569806301712e-08,"hate/threatening":0.003288434585556388,"violence/graphic":3.2010063932830235e-08}}]}',
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
assertEquals((await new OpenAIPolicy({ apiKey: '', fetch }).call(event, AbortSignal.timeout(timeout)))[2], false);
|
|
22
|
+
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, timeout));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
Deno.test('accepts unflagged events', async () => {
|
|
27
|
+
const event = finalizeEvent(
|
|
28
|
+
{ kind: 1, content: 'I want to love them.', tags: [], created_at: 0 },
|
|
29
|
+
generateSecretKey(),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const fetch = () =>
|
|
33
|
+
Promise.resolve(
|
|
34
|
+
new Response(
|
|
35
|
+
'{"id":"modr-6zvS6HoiwBqOQ9VYSggGAAI9vSgWD","model":"text-moderation-004","results":[{"flagged":false,"categories":{"sexual":false,"hate":false,"violence":false,"self-harm":false,"sexual/minors":false,"hate/threatening":false,"violence/graphic":false},"category_scores":{"sexual":1.94798508346139e-06,"hate":2.756592039077077e-07,"violence":1.4010063864589029e-07,"self-harm":3.1806530742528594e-09,"sexual/minors":1.8928545841845335e-08,"hate/threatening":3.1036221769670247e-12,"violence/graphic":1.5348690096672613e-09}}]}',
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
assertEquals((await new OpenAIPolicy({ apiKey: '', fetch }).call(event, AbortSignal.timeout(timeout)))[2], false);
|
|
40
|
+
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, timeout));
|
|
42
|
+
});
|
package/OpenAIPolicy.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { 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
|
+
constructor(private opts: OpenAIPolicyOpts) {}
|
|
79
|
+
|
|
80
|
+
async call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK> {
|
|
81
|
+
const {
|
|
82
|
+
handler = (_, { results }) => results.some((r) => r.flagged),
|
|
83
|
+
endpoint = 'https://api.openai.com/v1/moderations',
|
|
84
|
+
kinds = [1, 30023],
|
|
85
|
+
apiKey,
|
|
86
|
+
} = this.opts;
|
|
87
|
+
|
|
88
|
+
if (kinds.includes(event.kind)) {
|
|
89
|
+
try {
|
|
90
|
+
const resp = await fetch(endpoint, {
|
|
91
|
+
headers: {
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
input: event.content,
|
|
97
|
+
}),
|
|
98
|
+
signal,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await resp.json();
|
|
102
|
+
|
|
103
|
+
if (handler(event, result)) {
|
|
104
|
+
return ['OK', event.id, false, 'blocked: content flagged by AI'];
|
|
105
|
+
}
|
|
106
|
+
} catch (_) {
|
|
107
|
+
return ['OK', event.id, false, 'blocked: error analyzing content'];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return ['OK', event.id, true, ''];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { NoOpPolicy } from './NoOpPolicy.ts';
|
|
5
|
+
import { PipePolicy } from './PipePolicy.ts';
|
|
6
|
+
import { ReadOnlyPolicy } from './ReadOnlyPolicy.ts';
|
|
7
|
+
|
|
8
|
+
Deno.test('passes events through multiple policies', async () => {
|
|
9
|
+
const policy = new PipePolicy([
|
|
10
|
+
new NoOpPolicy(),
|
|
11
|
+
new ReadOnlyPolicy(),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const event = finalizeEvent(
|
|
15
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
16
|
+
generateSecretKey(),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const [_, _eventId, ok, reason] = await policy.call(event);
|
|
20
|
+
|
|
21
|
+
assertEquals(ok, false);
|
|
22
|
+
assertEquals(reason, 'blocked: the relay is read-only');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
Deno.test('short-circuits on the first reject', async () => {
|
|
26
|
+
const policy = new PipePolicy([
|
|
27
|
+
new ReadOnlyPolicy(),
|
|
28
|
+
new NoOpPolicy(),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const event = finalizeEvent(
|
|
32
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
33
|
+
generateSecretKey(),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const [_, _eventId, ok, reason] = await policy.call(event);
|
|
37
|
+
|
|
38
|
+
assertEquals(ok, false);
|
|
39
|
+
assertEquals(reason, 'blocked: the relay is read-only');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
Deno.test('accepts when all policies accept', async () => {
|
|
43
|
+
const policy = new PipePolicy([
|
|
44
|
+
new NoOpPolicy(),
|
|
45
|
+
new NoOpPolicy(),
|
|
46
|
+
new NoOpPolicy(),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const event = finalizeEvent(
|
|
50
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
51
|
+
generateSecretKey(),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const [_, _eventId, ok, reason] = await policy.call(event);
|
|
55
|
+
|
|
56
|
+
assertEquals(ok, true);
|
|
57
|
+
assertEquals(reason, '');
|
|
58
|
+
});
|
package/PipePolicy.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { 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
|
+
constructor(private policies: NPolicy[]) {}
|
|
24
|
+
|
|
25
|
+
async call(event: NostrEvent, signal?: AbortSignal): Promise<NostrRelayOK> {
|
|
26
|
+
for (const policy of this.policies) {
|
|
27
|
+
const [_, eventId, ok, reason] = await policy.call(event, signal);
|
|
28
|
+
|
|
29
|
+
if (!ok) {
|
|
30
|
+
return [_, eventId, ok, reason];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return ['OK', event.id, true, ''];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { PowPolicy } from './PowPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('blocks events without a nonce', async () => {
|
|
7
|
+
const event = finalizeEvent(
|
|
8
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
9
|
+
generateSecretKey(),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
assertEquals((await new PowPolicy().call(event))[2], false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
Deno.test('accepts event with sufficient POW', async () => {
|
|
16
|
+
const event = {
|
|
17
|
+
id: '000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358',
|
|
18
|
+
tags: [['nonce', '776797', '20']],
|
|
19
|
+
kind: 1,
|
|
20
|
+
content: '',
|
|
21
|
+
pubkey: '',
|
|
22
|
+
created_at: 0,
|
|
23
|
+
sig: '',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
assertEquals((await new PowPolicy().call(event))[2], true);
|
|
27
|
+
});
|
package/PowPolicy.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayInfo, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
import { nip13 } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
/** Policy options for `PowPolicy`. */
|
|
5
|
+
interface PowPolicyOpts {
|
|
6
|
+
/** Events will be rejected if their `id` does not contain at least this many leading 0 bits. Default: `1` */
|
|
7
|
+
difficulty?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reject events which don't meet Proof-of-Work ([NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md)) criteria.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* new PowPolicy({ difficulty: 20 });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class PowPolicy implements NPolicy {
|
|
18
|
+
constructor(private opts: PowPolicyOpts = {}) {}
|
|
19
|
+
|
|
20
|
+
// deno-lint-ignore require-await
|
|
21
|
+
async call({ id, tags }: NostrEvent): Promise<NostrRelayOK> {
|
|
22
|
+
const { difficulty = 1 } = this.opts;
|
|
23
|
+
|
|
24
|
+
const pow = nip13.getPow(id);
|
|
25
|
+
const nonce = tags.find(([name]) => name === 'nonce');
|
|
26
|
+
|
|
27
|
+
if (pow >= difficulty && nonce && Number(nonce[2]) >= difficulty) {
|
|
28
|
+
return ['OK', id, true, ''];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return ['OK', id, false, `pow: insufficient proof-of-work (difficulty ${difficulty})`];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get info(): NostrRelayInfo {
|
|
35
|
+
return {
|
|
36
|
+
limitation: {
|
|
37
|
+
min_pow_difficulty: this.opts.difficulty,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { assertEquals } from '@std/assert';
|
|
2
|
+
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
|
|
3
|
+
|
|
4
|
+
import { PubkeyBanPolicy } from './PubkeyBanPolicy.ts';
|
|
5
|
+
|
|
6
|
+
Deno.test('PubkeyBanPolicy', async () => {
|
|
7
|
+
const [event1, event2, event3] = new Array(3).fill(0).map(() => {
|
|
8
|
+
return finalizeEvent(
|
|
9
|
+
{ kind: 1, content: '', tags: [], created_at: 0 },
|
|
10
|
+
generateSecretKey(),
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
assertEquals((await new PubkeyBanPolicy([]).call(event1))[2], true);
|
|
15
|
+
assertEquals((await new PubkeyBanPolicy([event2.pubkey, event1.pubkey]).call(event3))[2], true);
|
|
16
|
+
assertEquals((await new PubkeyBanPolicy([event2.pubkey, event1.pubkey]).call(event2))[2], false);
|
|
17
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ban events from individual pubkeys.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Ban a specific pubkey.
|
|
8
|
+
* new PubkeyBanPolicy(['e810fafa1e89cdf80cced8e013938e87e21b699b24c8570537be92aec4b12c18']);
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export class PubkeyBanPolicy implements NPolicy {
|
|
12
|
+
constructor(private pubkeys: Iterable<string>) {}
|
|
13
|
+
|
|
14
|
+
// deno-lint-ignore require-await
|
|
15
|
+
async call({ id, pubkey }: NostrEvent): Promise<NostrRelayOK> {
|
|
16
|
+
for (const p of this.pubkeys) {
|
|
17
|
+
if (p === pubkey) {
|
|
18
|
+
return ['OK', id, false, 'blocked: pubkey is banned'];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return ['OK', id, true, ''];
|
|
23
|
+
}
|
|
24
|
+
}
|