This commit is contained in:
Your Name 2024-09-15 15:51:23 -06:00
commit c0dc97f660
45 changed files with 2703 additions and 0 deletions

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

259
README.md Normal file
View File

@ -0,0 +1,259 @@
# strfry policies
A collection of policies for the [strfry](https://github.com/hoytech/strfry) Nostr relay, built in Deno.
For more information about policy plugins, see [strfry: Write policy plugins](https://github.com/hoytech/strfry/blob/master/docs/plugins.md).
This library introduces a model for writing policies and composing them in a pipeline. Policies are fully configurable and it's easy to add your own or install more from anywhere on the net.
![Screenshot_from_2023-03-29_23-09-09](https://gitlab.com/soapbox-pub/strfry-policies/uploads/4a95c433eb6b4b0ca74f5c5e71f27d7b/Screenshot_from_2023-03-29_23-09-09.png)
## Getting started
To get up and running, you will need to install Deno on the same machine as strfry:
```sh
sudo apt install -y unzip
curl -fsSL https://deno.land/x/install/install.sh | sudo DENO_INSTALL=/usr/local sh
```
Create an entrypoint file somewhere and make it executable:
```sh
sudo touch /opt/strfry-policy.ts
sudo chmod +x /opt/strfry-policy.ts
```
Now you can write your policy. Here's a good starting point:
```ts
#!/bin/sh
//bin/true; exec deno run -A "$0" "$@"
import {
antiDuplicationPolicy,
hellthreadPolicy,
pipeline,
rateLimitPolicy,
readStdin,
writeStdout,
} from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
for await (const msg of readStdin()) {
const result = await pipeline(msg, [
[hellthreadPolicy, { limit: 100 }],
[antiDuplicationPolicy, { ttl: 60000, minLength: 50 }],
[rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
]);
writeStdout(result);
}
```
Finally, edit `strfry.conf` and enable the policy:
```diff
writePolicy {
# If non-empty, path to an executable script that implements the writePolicy plugin logic
- plugin = ""
+ plugin = "/opt/strfry-policy.ts"
# Number of seconds to search backwards for lookback events when starting the writePolicy plugin (0 for no lookback)
lookbackSeconds = 0
```
That's it! 🎉 Now you should check strfry logs to ensure everything is working okay.
## Available policies
For complete documentation of policies, see: https://doc.deno.land/https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts
| Policy | Description | Example Options |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `antiDuplicationPolicy` | Prevent messages with the exact same content from being submitted repeatedly. | `{ ttl: 60000, minLength: 50}` |
| `filterPolicy` | Reject all events which don't match the filter. | `{ kinds: [0, 1, 3, 5, 6, 7] }` |
| `hellthreadPolicy` | Reject messages that tag too many participants. | `{ limit: 15 }` |
| `keywordPolicy` | Reject events containing any of the strings in its content. | `['moo', 'oink', 'honk']` |
| `noopPolicy` | Minimal sample policy for demonstration purposes. Allows all events through. | |
| `openaiPolicy` | Passes event content to OpenAI and then rejects flagged events. | `{ apiKey: '123...' }` |
| `powPolicy` | Reject events which don't meet Proof-of-Work ([NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md)) criteria. | `{ difficulty: 20 }` |
| `pubkeyBanPolicy` | Ban individual pubkeys from publishing events to the relay. | `['e810...', 'fafa...', '1e89...']` |
| `rateLimitPolicy` | Rate-limits users by their IP address. | `{ max: 10, interval: 60000 }` |
| `readOnlyPolicy` | This policy rejects all messages. | |
| `regexPolicy` | Reject events whose content matches the regex. | `/(🟠\|🔥\|😳)ChtaGPT/i` |
| `whitelistPolicy` | Allows only the listed pubkeys to post to the relay. All other events are rejected. | `['e810...', 'fafa...', '1e89...']` |
## Upgrading strfry-policies
When writing your script, it's a good idea to import the module with a permalink, eg:
```diff
- import * as strfry from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
+ import * as strfry from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/33ef127ca7599d9d7016786cbe2de34c9536078c/mod.ts';
```
You can also import from a tag:
```diff
- import * as strfry from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
+ import * as strfry from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/v0.1.0/mod.ts';
```
Therefore, to upgrade to a newer version of strfry-policies, you can simply change the import URL.
## Writing your own policies
You can write a policy in TypeScript and host it anywhere. Deno allows importing modules by URL, making it easy to share policies.
Here is a basic sample policy:
```ts
import type { Policy } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
/** Only American English is allowed. */
const americanPolicy: Policy<void> = (msg) => {
const { content } = msg.event;
const words = [
'armour',
'behaviour',
'colour',
'favourite',
'flavour',
'honour',
'humour',
'rumour',
];
const isBritish = words.some((word) => content.toLowerCase().includes(word));
if (isBritish) {
return {
id: msg.event.id,
action: 'reject',
msg: 'Sorry, only American English is allowed on this server!',
};
} else {
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
}
};
export default americanPolicy;
```
Once you're done, you can either upload the file somewhere online or directly to your server. Then, update your pipeline:
```diff
--- a/strfry-policy.ts
+++ b/strfry-policy.ts
@@ -8,12 +8,14 @@ import {
readStdin,
writeStdout,
} from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
+import { americanPolicy } from 'https://gist.githubusercontent.com/alexgleason/5c2d084434fa0875397f44da198f4352/raw/3d3ce71c7ed9cef726f17c3a102c378b81760a45/american-policy.ts';
for await (const msg of readStdin()) {
const result = await pipeline(msg, [
[hellthreadPolicy, { limit: 100 }],
[antiDuplicationPolicy, { ttl: 60000, minLength: 50 }],
[rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
+ americanPolicy,
]);
writeStdout(result);
```
### Policy options
The `Policy<Opts>` type is a generic that accepts options of any type. With opts, the policy above could be rewritten as:
```diff
--- a/american-policy.ts
+++ b/american-policy.ts
@@ -1,7 +1,11 @@
import type { Policy } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
+interface American {
+ withGrey?: boolean;
+}
+
/** Only American English is allowed. */
-const americanPolicy: Policy<void> = (msg) => {
+const americanPolicy: Policy<American> = (msg, opts) => {
const { content } = msg.event;
const words = [
@@ -15,6 +19,10 @@
'rumour',
];
+ if (opts?.withGrey) {
+ words.push('grey');
+ }
+
const isBritish = words.some((word) => content.toLowerCase().includes(word));
if (isBritish) {
```
Then, in the pipeline:
```diff
- americanPolicy,
+ [americanPolicy, { withGrey: true }],
```
### Caveats
- You should not use `console.log` anywhere in your policies, as strfry expects stdout to be the strfry output message.
## Usage with Node.js
We highly recommend running this library with Deno, but for those looking to incorporate it into an existing Node.js project, an NPM version is provided:
- https://www.npmjs.com/package/strfry-policies
This version is built with [dnt](https://github.com/denoland/dnt) which provides Node.js shims for Deno features. Some policies that rely on sqlite may not work, but core fuctionality and TypeScript types work fine, so it can be used as a framework to build other policies.
## Filtering jsonl events with your policy
It is not currently possible to retroactively filter events on your strfry relay. You can however export the events with `strfry export`, filter them locally, and then import them into a fresh database. You can also use this command to filter Nostr events from any source, not just strfry.
To do so, run:
```sh
cat [EVENTS_FILE] | deno task filter [POLICY_CMD] > [OUT_FILE]
```
For example:
```sh
cat events.jsonl | deno task filter ./my-policy.ts > filtered.jsonl
```
Accepted messages will be written to stdout, while rejected messages will be skipped. Also, `[POLICY_CMD]` can be _any_ strfry policy, not just one created from this repo.
The command wraps each event in a strfry message of type `new`, with an `IP4` source of `127.0.0.1`, and a timestamp of the current UTC time. Therefore you may want to avoid certain policies such as the `rateLimitPolicy` that don't makes sense in this context.
## FAQ
### Why Deno?
Deno was developed by the creator of Node.js to solve various problems with Node. It implements web standard APIs such as SubtleCrypto and Fetch so your code is compatible with web browsers. It is also significantly faster in benchmarks.
### Can I integrate this into another project?
If you're building your own relay, make it compatible with [strfry plugins](https://github.com/hoytech/strfry/blob/master/docs/plugins.md). strfry plugins utilize stdin and stdout of the operating system, so the relay can be written in any programming language and it will be able to utilize plugins written in any programming language.
If you're writing software that deals with Nostr events in JavaScript or TypeScript, you can import this library and use its functions directly.
### Is performance good?
It depends on which policies you use, but the short answer is "yes." Even policies that rely on sqlite (such as `rateLimitPolicy` and `antiDuplicationPolicy`) perform well. You should place those policies at the end of your pipeline so that synchronous policies have the chance to reject sooner. You can also mount a tmpfs volume and pass a `databaseUrl` option into those policies to serve their database from memory.
## License
This is free and unencumbered software released into the public domain.

40
deno.json Normal file
View File

@ -0,0 +1,40 @@
{
"tasks": {
"test": "deno test --allow-read --allow-write",
"filter": "deno run -A scripts/filter.ts",
"npm": "deno run -A scripts/npm.ts"
},
"test": {
"files": {
"exclude": ["npm/"]
}
},
"lock": {
"frozen": false
},
"lint": {
"files": {
"include": ["."],
"exclude": ["npm/"]
},
"rules": {
"tags": ["recommended"],
"exclude": ["no-explicit-any"]
}
},
"fmt": {
"files": {
"include": ["."],
"exclude": ["npm/"]
},
"options": {
"useTabs": false,
"lineWidth": 120,
"indentWidth": 2,
"semiColons": true,
"singleQuote": true,
"proseWrap": "preserve"
}
}
}

73
deno.lock generated Normal file
View File

@ -0,0 +1,73 @@
{
"version": "3",
"remote": {
"https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
"https://deno.land/std@0.181.0/bytes/bytes_list.ts": "b4cbdfd2c263a13e8a904b12d082f6177ea97d9297274a4be134e989450dfa6a",
"https://deno.land/std@0.181.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2",
"https://deno.land/std@0.181.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219",
"https://deno.land/std@0.181.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
"https://deno.land/std@0.181.0/io/buf_reader.ts": "abeb92b18426f11d72b112518293a96aef2e6e55f80b84235e8971ac910affb5",
"https://deno.land/std@0.181.0/io/buf_writer.ts": "48c33c8f00b61dcbc7958706741cec8e59810bd307bc6a326cbd474fe8346dfd",
"https://deno.land/std@0.181.0/io/buffer.ts": "17f4410eaaa60a8a85733e8891349a619eadfbbe42e2f319283ce2b8f29723ab",
"https://deno.land/std@0.181.0/io/copy_n.ts": "0cc7ce07c75130f6fc18621ec1911c36e147eb9570664fee0ea12b1988167590",
"https://deno.land/std@0.181.0/io/limited_reader.ts": "6c9a216f8eef39c1ee2a6b37a29372c8fc63455b2eeb91f06d9646f8f759fc8b",
"https://deno.land/std@0.181.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b",
"https://deno.land/std@0.181.0/io/multi_reader.ts": "9c2a0a31686c44b277e16da1d97b4686a986edcee48409b84be25eedbc39b271",
"https://deno.land/std@0.181.0/io/read_delim.ts": "c02b93cc546ae8caad8682ae270863e7ace6daec24c1eddd6faabc95a9d876a3",
"https://deno.land/std@0.181.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f",
"https://deno.land/std@0.181.0/io/read_lines.ts": "c526c12a20a9386dc910d500f9cdea43cba974e853397790bd146817a7eef8cc",
"https://deno.land/std@0.181.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e",
"https://deno.land/std@0.181.0/io/read_range.ts": "28152daf32e43dd9f7d41d8466852b0d18ad766cd5c4334c91fef6e1b3a74eb5",
"https://deno.land/std@0.181.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20",
"https://deno.land/std@0.181.0/io/read_string_delim.ts": "5dc9f53bdf78e7d4ee1e56b9b60352238ab236a71c3e3b2a713c3d78472a53ce",
"https://deno.land/std@0.181.0/io/slice_long_to_bytes.ts": "48d9bace92684e880e46aa4a2520fc3867f9d7ce212055f76ecc11b22f9644b7",
"https://deno.land/std@0.181.0/io/string_reader.ts": "da0f68251b3d5b5112485dfd4d1b1936135c9b4d921182a7edaf47f74c25cc8f",
"https://deno.land/std@0.181.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e",
"https://deno.land/std@0.181.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
"https://deno.land/std@0.181.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
"https://deno.land/std@0.181.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f",
"https://deno.land/std@0.88.0/async/deferred.ts": "f89ed49ba5e1dd0227c6bd5b23f017be46c3f92e4f0338dda08ff5aa54b9f6c9",
"https://deno.land/std@0.88.0/async/delay.ts": "9de1d8d07d1927767ab7f82434b883f3d8294fb19cad819691a2ad81a728cf3d",
"https://deno.land/std@0.88.0/async/mod.ts": "253b41c658d768613eacfb11caa0a9ca7148442f932018a45576f7f27554c853",
"https://deno.land/std@0.88.0/async/mux_async_iterator.ts": "b9091909db04cdb0af6f7807677372f64c1488de6c4bd86004511b064bf230d6",
"https://deno.land/std@0.88.0/async/pool.ts": "876f9e6815366cd017a3b4fbb9e9ae40310b1b6972f1bd541c94358bc11fb7e5",
"https://deno.land/std@0.88.0/encoding/base64.ts": "eecae390f1f1d1cae6f6c6d732ede5276bf4b9cd29b1d281678c054dc5cc009e",
"https://deno.land/std@0.88.0/encoding/hex.ts": "f952e0727bddb3b2fd2e6889d104eacbd62e92091f540ebd6459317a61932d9b",
"https://deno.land/std@0.88.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4",
"https://deno.land/std@0.88.0/node/_utils.ts": "067c386d676432e9418808851e8de72df7774f009a652904f62358b4c94504cf",
"https://deno.land/std@0.88.0/node/buffer.ts": "e98af24a3210d8fc3f022b6eb26d6e5bdf98fb0e02931e5983d20db9fed1b590",
"https://deno.land/std@0.88.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3",
"https://deno.land/std@0.88.0/testing/asserts.ts": "7fae8128125106ddf8e4b3ac84cc3b5fb2378e3fbf8ba38947ebe24faa002ce2",
"https://deno.land/x/module_cache@0.0.3/mod.ts": "c5e724477146e68b7a4d7ba440cd18f2ef4b28e4244ce48358c79efe98e3cd24",
"https://deno.land/x/sqlite@v3.7.1/build/sqlite.js": "c59f109f100c2bae0b9342f04e0d400583e2e3211d08bb71095177a4109ee5bf",
"https://deno.land/x/sqlite@v3.7.1/build/vfs.js": "08533cc78fb29b9d9bd62f6bb93e5ef333407013fed185776808f11223ba0e70",
"https://deno.land/x/sqlite@v3.7.1/mod.ts": "e09fc79d8065fe222578114b109b1fd60077bff1bb75448532077f784f4d6a83",
"https://deno.land/x/sqlite@v3.7.1/src/constants.ts": "90f3be047ec0a89bcb5d6fc30db121685fc82cb00b1c476124ff47a4b0472aa9",
"https://deno.land/x/sqlite@v3.7.1/src/db.ts": "59c6c2b5c4127132558bb8c610eadd811822f1a5d7f9c509704179ca192f94e0",
"https://deno.land/x/sqlite@v3.7.1/src/error.ts": "f7a15cb00d7c3797da1aefee3cf86d23e0ae92e73f0ba3165496c3816ab9503a",
"https://deno.land/x/sqlite@v3.7.1/src/function.ts": "e4c83b8ec64bf88bafad2407376b0c6a3b54e777593c70336fb40d43a79865f2",
"https://deno.land/x/sqlite@v3.7.1/src/query.ts": "d58abda928f6582d77bad685ecf551b1be8a15e8e38403e293ec38522e030cad",
"https://deno.land/x/sqlite@v3.7.1/src/wasm.ts": "e79d0baa6e42423257fb3c7cc98091c54399254867e0f34a09b5bdef37bd9487",
"https://esm.sh/nostr-tools@1.8.4?pin=v115": "62e5b620dbbaea0ee399efcc700260da12836a353fa521d35969d3454e591a77",
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/_assert.js": "2d47b1ae1c443fbcda3aa75e6d66c26da566d1775dcd757165314e8e9d1162da",
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/crypto.js": "0880be2fb91177484b9a5916a286aadce6a1c8b1b5cf6be47393361e6b121a17",
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/hmac.js": "cdb442a8326674449570b98daa44b07317908eae81205c178cab542ea754b91d",
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/pbkdf2.js": "e8b8e2ff70ecb35442fabfece10e76850ac8dc6aaf44a769871c9e6dbe60d264",
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/ripemd160.js": "8cd5e59afc12f6f6a2c980495f699a76d812ca30772d4c085ff8477fe4b1a2fe",
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/sha256.js": "8dec7d1bb4d0799f9cdf8f9ea7d8c3e91790255d547defcf62a626a0a190185e",
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/sha512.js": "85ccf57544faca95a6aeab11951f98f49e56b3cbad0618f624838c7e8fb4361d",
"https://esm.sh/v115/@noble/hashes@1.2.0/denonext/utils.js": "11431fc23031cb324977bc992e699fda8ec7c63fcc17c2b4f71a3902d48e99e5",
"https://esm.sh/v115/@noble/secp256k1@1.7.0/denonext/secp256k1.mjs": "36fb68b95b2f62de23d275be52b2eec68813083b93b78f7032492188ef59c77b",
"https://esm.sh/v115/@noble/secp256k1@1.7.1/denonext/secp256k1.mjs": "43c5a7ba14ae81b36e5ce64abf45962119527e926cddb764b7e510869b05f0bd",
"https://esm.sh/v115/@scure/base@1.1.1/denonext/base.mjs": "8f9cb853c4f6a4367c2f5bfb921d54b4ed61e41829944435e5878781b54d94a9",
"https://esm.sh/v115/@scure/bip32@1.1.4/denonext/bip32.mjs": "05471356192b1286874be6c28bea4ebac6dd6bc680bce795640604bb317c2165",
"https://esm.sh/v115/@scure/bip39@1.1.1/denonext/bip39.mjs": "00ccac2e221996db35b6780b3ae2cf37a153111bd1d348c9defe3a4341ec683d",
"https://esm.sh/v115/@scure/bip39@1.1.1/denonext/wordlists/english.js": "72ca7f3b2e856a62caa00441579008da89ea21a9c8a428ae547cdcffd17ae40c",
"https://esm.sh/v115/nostr-tools@1.8.4/denonext/nostr-tools.mjs": "f8023312404e4a83f0c052653643bcdbf5169a1585bd5399f11c65f37f7bcf16",
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/adapter.ts": "32e5182648011b188952ada0528f564b374260449ec3b06237f36225d4d19510",
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/jsonb.ts": "1b540f8bd0b43fe847cd3e2a852d2f53e610cd77b81c11d175ebe91a3f110be8",
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/keydb.ts": "616c4c866c9e11c29d5654d367468ed51b689565043f53fdeb5eb66f25138156",
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/memory.ts": "f0ab6faf293c4ad3539fd3cf89c764d7f34d39d24e471ea59eebb5d1f5a510dc",
"https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/sqlite.ts": "c8f172cfea9425cb16e844622375c9578db508de7d710ad3987cf6cd6bff197a"
}
}

30
entrypoint.example.ts Executable file
View File

@ -0,0 +1,30 @@
#!/bin/sh
//bin/true; exec deno run -A "$0" "$@"
import {
antiDuplicationPolicy,
filterPolicy,
hellthreadPolicy,
keywordPolicy,
noopPolicy,
pipeline,
pubkeyBanPolicy,
rateLimitPolicy,
readStdin,
regexPolicy,
writeStdout,
} from './mod.ts';
for await (const msg of readStdin()) {
const result = await pipeline(msg, [
noopPolicy,
[filterPolicy, { kinds: [0, 1, 3, 5, 7, 1984, 9734, 9735, 10002] }],
[keywordPolicy, ['https://t.me/']],
[regexPolicy, /(🟠|🔥|😳)ChtaGPT/i],
[pubkeyBanPolicy, ['e810fafa1e89cdf80cced8e013938e87e21b699b24c8570537be92aec4b12c18']],
[hellthreadPolicy, { limit: 100 }],
[rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
[antiDuplicationPolicy, { ttl: 60000, minLength: 50 }],
]);
writeStdout(result);
}

19
mod.ts Normal file
View File

@ -0,0 +1,19 @@
export { type AntiDuplication, default as antiDuplicationPolicy } from './src/policies/anti-duplication-policy.ts';
export { default as filterPolicy, type Filter } from './src/policies/filter-policy.ts';
export { default as hellthreadPolicy, type Hellthread } from './src/policies/hellthread-policy.ts';
export { default as keywordPolicy } from './src/policies/keyword-policy.ts';
export { default as noopPolicy } from './src/policies/noop-policy.ts';
export { default as openaiPolicy, type OpenAI, type OpenAIHandler } from './src/policies/openai-policy.ts';
export { default as powPolicy, type POW } from './src/policies/pow-policy.ts';
export { default as pubkeyBanPolicy } from './src/policies/pubkey-ban-policy.ts';
export { default as rateLimitPolicy, type RateLimit } from './src/policies/rate-limit-policy.ts';
export { default as readOnlyPolicy } from './src/policies/read-only-policy.ts';
export { default as regexPolicy } from './src/policies/regex-policy.ts';
export { default as whitelistPolicy } from './src/policies/whitelist-policy.ts';
export { default as tagPolicy } from './src/policies/tagPolicy.ts';
export { default as replyGuy } from './src/policies/replyGuy.ts';
export { readStdin, writeStdout } from './src/io.ts';
export { default as pipeline, type PolicyTuple } from './src/pipeline.ts';
export type { Event, InputMessage, IterablePubkeys, OutputMessage, Policy } from './src/types.ts';

19
replyGuy.ts Normal file
View File

@ -0,0 +1,19 @@
import { Policy } from '../types.ts';
const replyGuy: Policy<RegExp> = ({ event: { id, content } }, regex) => {
if (regex?.test(content)) {
return {
id,
action: 'reject',
msg: 'blocked: Reply Guy',
};
}
return {
id,
action: 'accept',
msg: '',
};
};
export default replyGuy;

42
scripts/filter.ts Normal file
View File

@ -0,0 +1,42 @@
import { readLines } from '../src/deps.ts';
import type { Event, InputMessage, OutputMessage } from '../mod.ts';
for await (const line of readLines(Deno.stdin)) {
const event: Event = JSON.parse(line);
const input: InputMessage = {
type: 'new',
event: event,
receivedAt: Date.now() / 1000,
sourceType: 'IP4',
sourceInfo: '127.0.0.1',
};
const policy = Deno.run({
cmd: Deno.args,
stdin: 'piped',
stdout: 'piped',
});
const bytes = new TextEncoder().encode(JSON.stringify(input));
const writer = policy.stdin.writable.getWriter();
const chunkSize = 4096;
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.slice(i, i + chunkSize);
await writer.write(chunk);
}
policy.stdin.close();
for await (const out of readLines(policy.stdout)) {
const msg: OutputMessage = JSON.parse(out);
if (msg.action === 'accept') {
console.log(line);
}
break;
}
}

32
scripts/npm.ts Normal file
View File

@ -0,0 +1,32 @@
import { build, emptyDir } from 'https://deno.land/x/dnt@0.34.0/mod.ts';
await emptyDir('./npm');
await build({
entryPoints: ['./mod.ts'],
outDir: './npm',
shims: {
deno: true,
undici: true,
},
package: {
name: 'strfry-policies',
version: Deno.args[0],
description: 'Configurable policies for the strfry Nostr relay.',
license: 'Unlicense',
repository: {
type: 'git',
url: 'git+https://gitlab.com/soapbox-pub/strfry-policies.git',
},
bugs: {
url: 'https://gitlab.com/soapbox-pub/strfry-policies/-/issues',
},
},
typeCheck: false,
test: false,
scriptModule: false,
postBuild() {
Deno.copyFileSync('LICENSE', 'npm/LICENSE');
Deno.copyFileSync('README.md', 'npm/README.md');
},
});

4
src/deps.ts Normal file
View File

@ -0,0 +1,4 @@
export { readLines } from 'https://deno.land/std@0.181.0/io/mod.ts';
export { assert, assertEquals } from 'https://deno.land/std@0.181.0/testing/asserts.ts';
export { Keydb } from 'https://raw.githubusercontent.com/alexgleason/Keydb/1bda308df9e589339532daf31f1717ef7a59d2af/sqlite.ts';
export { type Filter, matchFilter, nip13 } from 'https://esm.sh/nostr-tools@1.8.4?pin=v115';

33
src/io.ts Normal file
View File

@ -0,0 +1,33 @@
import { readLines } from './deps.ts';
import type { InputMessage, OutputMessage } from './types.ts';
/**
* Parse strfy messages from stdin.
* strfry may batch multiple messages at once.
*
* @example
* ```ts
* // Loop through strfry input messages
* for await (const msg of readStdin()) {
* // handle `msg`
* }
* ```
*/
async function* readStdin(): AsyncGenerator<InputMessage> {
for await (const line of readLines(Deno.stdin)) {
try {
yield JSON.parse(line);
} catch (e) {
console.error(line);
throw e;
}
}
}
/** Writes the output message to stdout. */
function writeStdout(msg: OutputMessage): void {
console.log(JSON.stringify(msg));
}
export { readStdin, writeStdout };

42
src/pipeline.test.ts Normal file
View File

@ -0,0 +1,42 @@
import { assertEquals } from './deps.ts';
import pipeline from './pipeline.ts';
import noopPolicy from './policies/noop-policy.ts';
import readOnlyPolicy from './policies/read-only-policy.ts';
import { buildInputMessage } from './test.ts';
Deno.test('passes events through multiple policies', async () => {
const msg = buildInputMessage();
const result = await pipeline(msg, [
noopPolicy,
readOnlyPolicy,
]);
assertEquals(result.action, 'reject');
assertEquals(result.msg, 'blocked: the relay is read-only');
});
Deno.test('short-circuits on the first reject', async () => {
const msg = buildInputMessage();
const result = await pipeline(msg, [
readOnlyPolicy,
noopPolicy,
]);
assertEquals(result.action, 'reject');
assertEquals(result.msg, 'blocked: the relay is read-only');
});
Deno.test('accepts when all policies accept', async () => {
const msg = buildInputMessage();
const result = await pipeline(msg, [
noopPolicy,
noopPolicy,
noopPolicy,
]);
assertEquals(result.action, 'accept');
});

57
src/pipeline.ts Normal file
View File

@ -0,0 +1,57 @@
import { InputMessage, OutputMessage, Policy } from './types.ts';
/** A policy function with opts to run it with. Used by the pipeline. */
type PolicyTuple<P extends Policy = Policy> = [policy: P, opts?: InferPolicyOpts<P>];
/** Infer opts from the policy. */
type InferPolicyOpts<P> = P extends Policy<infer Opts> ? Opts : never;
/** Helper type for proper type inference of PolicyTuples. */
// https://stackoverflow.com/a/75806165
// https://stackoverflow.com/a/54608401
type Policies<T extends any[]> = {
[K in keyof T]: PolicyTuple<T[K]> | Policy<T[K]>;
};
/**
* Processes messages through multiple policies.
*
* If any policy returns a `reject` or `shadowReject` action, the pipeline will stop and return the rejected message.
*
* @example
* ```ts
* const result = await pipeline(msg, [
* noopPolicy,
* [filterPolicy, { kinds: [0, 1, 3, 5, 7, 1984, 9734, 9735, 10002] }],
* [keywordPolicy, ['https://t.me/']],
* [regexPolicy, /(🟠|🔥|😳)ChtaGPT/i],
* [pubkeyBanPolicy, ['e810fafa1e89cdf80cced8e013938e87e21b699b24c8570537be92aec4b12c18']],
* [hellthreadPolicy, { limit: 100 }],
* [rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
* [antiDuplicationPolicy, { ttl: 60000, minLength: 50 }],
* ]);
* ```
*/
async function pipeline<T extends unknown[]>(msg: InputMessage, policies: [...Policies<T>]): Promise<OutputMessage> {
for (const item of policies as (Policy | PolicyTuple)[]) {
const [policy, opts] = toTuple(item);
const result = await policy(msg, opts);
if (result.action !== 'accept') {
return result;
}
}
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
}
/** Coerce item into a tuple if it isn't already. */
function toTuple<P extends Policy>(item: PolicyTuple<P> | P): PolicyTuple<P> {
return typeof item === 'function' ? [item] : item;
}
export default pipeline;
export type { PolicyTuple };

View File

@ -0,0 +1,24 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import antiDuplicationPolicy from './anti-duplication-policy.ts';
Deno.test({
name: 'blocks events that post the same content too quickly',
fn: async () => {
const content = 'Spicy peppermint apricot mediterranean ginger carrot spiced juice edamame hummus';
const msg1 = buildInputMessage({ event: buildEvent({ content }) });
assertEquals((await antiDuplicationPolicy(msg1)).action, 'accept');
assertEquals((await antiDuplicationPolicy(msg1)).action, 'shadowReject');
assertEquals((await antiDuplicationPolicy(msg1)).action, 'shadowReject');
const msg2 = buildInputMessage({ event: buildEvent({ content: 'a' }) });
assertEquals((await antiDuplicationPolicy(msg2)).action, 'accept');
assertEquals((await antiDuplicationPolicy(msg2)).action, 'accept');
assertEquals((await antiDuplicationPolicy(msg2)).action, 'accept');
},
sanitizeResources: false,
});

View File

@ -0,0 +1,82 @@
import { Keydb } from '../deps.ts';
import type { Policy } from '../types.ts';
/** Policy options for `antiDuplicationPolicy`. */
interface AntiDuplication {
/** Time in ms until a message with this content may be posted again. Default: `60000` (1 minute). */
ttl?: number;
/** Note text under this limit will be skipped by the policy. Default: `50`. */
minLength?: number;
/** Database connection string. Default: `sqlite:///tmp/strfry-anti-duplication-policy.sqlite3` */
databaseUrl?: string;
}
/**
* Prevent messages with the exact same content from being submitted repeatedly.
*
* It stores a hashcode for each content in an SQLite database and rate-limits them. Only messages that meet the minimum length criteria are selected.
* 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.
*
* @example
* ```ts
* // Prevent the same message from being posted within 60 seconds.
* antiDuplicationPolicy(msg, { ttl: 60000 });
*
* // Only enforce the policy on messages with at least 50 characters.
* antiDuplicationPolicy(msg, { ttl: 60000, minLength: 50 });
*
* // Use a custom SQLite path.
* antiDuplicationPolicy(msg, { databaseUrl: 'sqlite:///media/ramdisk/nostr-contenthash.sqlite3' });
* ```
*/
const antiDuplicationPolicy: Policy<AntiDuplication> = async (msg, opts = {}) => {
const {
ttl = 60000,
minLength = 50,
databaseUrl = 'sqlite:///tmp/strfry-anti-duplication-policy.sqlite3',
} = opts;
const { kind, content } = msg.event;
if (kind === 1 && content.length >= minLength) {
const db = new Keydb(databaseUrl);
const hash = String(hashCode(content));
if (await db.get(hash)) {
await db.set(hash, 1, ttl);
return {
id: msg.event.id,
action: 'shadowReject',
msg: '',
};
}
await db.set(hash, 1, ttl);
}
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
/**
* Get a "good enough" unique identifier for this content.
* This algorithm was chosen because it's very fast with a low chance of collisions.
* https://stackoverflow.com/a/8831937
*/
function hashCode(str: string): number {
let hash = 0;
for (let i = 0, len = str.length; i < len; i++) {
const chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
export default antiDuplicationPolicy;
export type { AntiDuplication };

View File

@ -0,0 +1,13 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import filterPolicy from './filter-policy.ts';
Deno.test('only allows events that match the filter', async () => {
const msg0 = buildInputMessage({ event: buildEvent({ kind: 0 }) });
const msg1 = buildInputMessage({ event: buildEvent({ kind: 1 }) });
assertEquals((await filterPolicy(msg0, { kinds: [1] })).action, 'reject');
assertEquals((await filterPolicy(msg1, { kinds: [1] })).action, 'accept');
assertEquals((await filterPolicy(msg1, { kinds: [1], authors: [] })).action, 'reject');
});

View File

@ -0,0 +1,34 @@
import { type Filter, matchFilter } from '../deps.ts';
import { Policy } from '../types.ts';
/**
* Reject all events which don't match the filter.
*
* Only messages which **match** the filter are allowed, and all others are dropped.
* The filter is a [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md) relay filter.
*
* @example
* ```ts
* // Only allow kind 1, 3, 5, and 7 events.
* filterPolicy(msg, { kinds: [0, 1, 3, 5, 6, 7] });
* ```
*/
const filterPolicy: Policy<Filter> = ({ event }, filter = {}) => {
if (matchFilter(filter, event)) {
return {
id: event.id,
action: 'accept',
msg: '',
};
}
return {
id: event.id,
action: 'reject',
msg: 'blocked: the event doesn\'t match the allowed filters',
};
};
export default filterPolicy;
export type { Filter };

View File

@ -0,0 +1,14 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import hellthreadPolicy from './hellthread-policy.ts';
Deno.test('blocks events with too many mentioned users', async () => {
const tags = [['p'], ['p'], ['p']];
const msg0 = buildInputMessage();
const msg1 = buildInputMessage({ event: buildEvent({ tags }) });
assertEquals((await hellthreadPolicy(msg0, { limit: 1 })).action, 'accept');
assertEquals((await hellthreadPolicy(msg1, { limit: 1 })).action, 'reject');
});

View File

@ -0,0 +1,45 @@
import type { Policy } from '../types.ts';
/** Policy options for `hellthreadPolicy`. */
interface Hellthread {
/** Total number of "p" tags a kind 1 note may have before it's rejected. Default: `100` */
limit?: number;
}
/**
* Reject messages that tag too many participants.
*
* This policy is useful to prevent unwanted notifications by limiting the number of "p" tags a kind 1 event may have.
* Only kind 1 events are impacted by this policy, since kind 3 events will commonly exceed this number.
*
* @example
* ```ts
* // Reject events with more than 15 mentions.
* hellthreadPolicy(msg, { limit: 15 });
* ```
*/
const hellthreadPolicy: Policy<Hellthread> = (msg, opts) => {
const limit = opts?.limit ?? 100;
if (msg.event.kind === 1) {
const p = msg.event.tags.filter((tag) => tag[0] === 'p');
if (p.length > limit) {
return {
id: msg.event.id,
action: 'reject',
msg: `blocked: rejected due to ${p.length} "p" tags (${limit} is the limit).`,
};
}
}
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
export default hellthreadPolicy;
export type { Hellthread };

View File

@ -0,0 +1,17 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import keywordPolicy from './keyword-policy.ts';
Deno.test('blocks banned pubkeys', async () => {
const words = ['https://t.me/spam', 'hello world'];
const msg0 = buildInputMessage();
const msg1 = buildInputMessage({ event: buildEvent({ content: '🔥🔥🔥 https://t.me/spam 我想死' }) });
const msg2 = buildInputMessage({ event: buildEvent({ content: 'hElLo wOrLd!' }) });
assertEquals((await keywordPolicy(msg0, words)).action, 'accept');
assertEquals((await keywordPolicy(msg1, words)).action, 'reject');
assertEquals((await keywordPolicy(msg1, [])).action, 'accept');
assertEquals((await keywordPolicy(msg2, words)).action, 'reject');
});

View File

@ -0,0 +1,30 @@
import { Policy } from '../types.ts';
/**
* Reject events containing any of the strings in its content.
*
* @example
* ```ts
* // Reject events with bad words.
* keywordPolicy(msg, ['moo', 'oink', 'honk']);
* ```
*/
const keywordPolicy: Policy<Iterable<string>> = ({ event: { id, content } }, words = []) => {
for (const word of words) {
if (content.toLowerCase().includes(word.toLowerCase())) {
return {
id,
action: 'reject',
msg: 'blocked: contains a banned word or phrase.',
};
}
}
return {
id,
action: 'accept',
msg: '',
};
};
export default keywordPolicy;

View File

@ -0,0 +1,9 @@
import { assertEquals } from '../deps.ts';
import { buildInputMessage } from '../test.ts';
import noopPolicy from './noop-policy.ts';
Deno.test('allows events', async () => {
const msg = buildInputMessage();
assertEquals((await noopPolicy(msg)).action, 'accept');
});

10
src/policies/noop-policy.ts Executable file
View File

@ -0,0 +1,10 @@
import type { Policy } from '../types.ts';
/** Minimal sample policy for demonstration purposes. Allows all events through. */
const noopPolicy: Policy<void> = (msg) => ({
id: msg.event.id,
action: 'accept',
msg: '',
});
export default noopPolicy;

View File

@ -0,0 +1,32 @@
import { MockFetch } from 'https://deno.land/x/deno_mock_fetch@1.0.1/mod.ts';
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import openaiPolicy from './openai-policy.ts';
const mockFetch = new MockFetch();
mockFetch.deactivateNetConnect();
mockFetch
.intercept('https://api.openai.com/v1/moderations', { body: '{"input":"I want to kill them."}' })
.response(
'{"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}}]}',
);
mockFetch
.intercept('https://api.openai.com/v1/moderations', { body: '{"input":"I want to love them."}' })
.response(
'{"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}}]}',
);
Deno.test('rejects flagged events', async () => {
const msg = buildInputMessage({ event: buildEvent({ content: 'I want to kill them.' }) });
assertEquals((await openaiPolicy(msg)).action, 'reject');
});
Deno.test('accepts unflagged events', async () => {
const msg = buildInputMessage({ event: buildEvent({ content: 'I want to love them.' }) });
assertEquals((await openaiPolicy(msg)).action, 'accept');
});

115
src/policies/openai-policy.ts Executable file
View File

@ -0,0 +1,115 @@
import type { Event, Policy } from '../types.ts';
/**
* OpenAI moderation result.
*
* https://platform.openai.com/docs/api-reference/moderations/create
*/
interface ModerationData {
id: string;
model: string;
results: {
categories: {
'hate': boolean;
'hate/threatening': boolean;
'self-harm': boolean;
'sexual': boolean;
'sexual/minors': boolean;
'violence': boolean;
'violence/graphic': boolean;
};
category_scores: {
'hate': number;
'hate/threatening': number;
'self-harm': number;
'sexual': number;
'sexual/minors': number;
'violence': number;
'violence/graphic': number;
};
flagged: boolean;
}[];
}
/**
* Callback for fine control over the policy. It contains the event and the OpenAI moderation data.
* Implementations should return `true` to **reject** the content, and `false` to accept.
*/
type OpenAIHandler = (event: Event, data: ModerationData) => boolean;
/** Policy options for `openaiPolicy`. */
interface OpenAI {
handler?: OpenAIHandler;
endpoint?: string;
apiKey?: string;
}
/** Default handler. Simply checks whether OpenAI flagged the content. */
const flaggedHandler: OpenAIHandler = (_, { results }) => results.some((r) => r.flagged);
/**
* Passes event content to OpenAI and then rejects flagged events.
*
* By default, this policy will reject kind 1 events that OpenAI flags.
* It's possible to pass a custom handler for more control. An OpenAI API key is required.
*
* @example
* ```ts
* // Default handler. It's so strict it's suitable for school children.
* openaiPolicy(msg, { apiKey: Deno.env.get('OPENAI_API_KEY') });
*
* // With a custom handler.
* openaiPolicy(msg, {
* apiKey: Deno.env.get('OPENAI_API_KEY'),
* handler(event, result) {
* // Loop each result.
* return data.results.some((result) => {
* if (result.flagged) {
* const { sexual, violence } = result.categories;
* // Reject only events flagged as sexual and violent.
* return sexual && violence;
* }
* });
* },
* });
* ```
*/
const openaiPolicy: Policy<OpenAI> = async ({ event }, opts = {}) => {
const {
handler = flaggedHandler,
endpoint = 'https://api.openai.com/v1/moderations',
apiKey,
} = opts;
if (event.kind === 1) {
const resp = await fetch(endpoint, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
input: event.content,
}),
});
const result = await resp.json();
if (handler(event, result)) {
return {
id: event.id,
action: 'reject',
msg: 'blocked: content flagged by AI',
};
}
}
return {
id: event.id,
action: 'accept',
msg: '',
};
};
export { flaggedHandler, openaiPolicy as default };
export type { ModerationData, OpenAI, OpenAIHandler };

View File

@ -0,0 +1,20 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import powPolicy from './pow-policy.ts';
Deno.test('blocks events without a nonce', async () => {
const msg = buildInputMessage();
assertEquals((await powPolicy(msg)).action, 'reject');
});
Deno.test('accepts event with sufficient POW', async () => {
const msg = buildInputMessage({
event: buildEvent({
id: '000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358',
tags: [['nonce', '776797', '20']],
}),
});
assertEquals((await powPolicy(msg, { difficulty: 20 })).action, 'accept');
});

View File

@ -0,0 +1,42 @@
import { nip13 } from '../deps.ts';
import type { Policy } from '../types.ts';
/** Policy options for `powPolicy`. */
interface POW {
/** Events will be rejected if their `id` does not contain at least this many leading 0 bits. Default: `1` */
difficulty?: number;
}
/**
* Reject events which don't meet Proof-of-Work ([NIP-13](https://github.com/nostr-protocol/nips/blob/master/13.md)) criteria.
*
* @example
* ```ts
* powPolicy(msg, { difficulty: 20 });
* ```
*/
const powPolicy: Policy<POW> = ({ event }, opts = {}) => {
const { difficulty = 1 } = opts;
const pow = nip13.getPow(event.id);
const nonce = event.tags.find((t) => t[0] === 'nonce');
if (pow >= difficulty && nonce && Number(nonce[2]) >= difficulty) {
return {
id: event.id,
action: 'accept',
msg: '',
};
}
return {
id: event.id,
action: 'reject',
msg: `pow: insufficient proof-of-work (difficulty ${difficulty})`,
};
};
export default powPolicy;
export type { POW };

View File

@ -0,0 +1,15 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import pubkeyBanPolicy from './pubkey-ban-policy.ts';
Deno.test('blocks banned pubkeys', async () => {
const msgA = buildInputMessage({ event: buildEvent({ pubkey: 'A' }) });
const msgB = buildInputMessage({ event: buildEvent({ pubkey: 'B' }) });
const msgC = buildInputMessage({ event: buildEvent({ pubkey: 'C' }) });
assertEquals((await pubkeyBanPolicy(msgA, [])).action, 'accept');
assertEquals((await pubkeyBanPolicy(msgA, ['A'])).action, 'reject');
assertEquals((await pubkeyBanPolicy(msgC, ['B', 'A'])).action, 'accept');
assertEquals((await pubkeyBanPolicy(msgB, ['B', 'A'])).action, 'reject');
});

View File

@ -0,0 +1,35 @@
import { IterablePubkeys, Policy } from '../types.ts';
/**
* Ban individual pubkeys from publishing events to the relay.
*
* @example
* ```ts
* // Ban a specific pubkey.
* pubkeyBanPolicy(msg, ['e810fafa1e89cdf80cced8e013938e87e21b699b24c8570537be92aec4b12c18']);
*
* // Load banned pubkeys from a text file.
* import { readLines } from 'https://deno.land/std/io/mod.ts';
* const pubkeys = readLines(await Deno.open('pubkeys.txt'));
* const result = await pubkeyBanPolicy(msg, pubkeys);
* ```
*/
const pubkeyBanPolicy: Policy<IterablePubkeys> = async ({ event: { id, pubkey } }, pubkeys = []) => {
for await (const p of pubkeys) {
if (p === pubkey) {
return {
id,
action: 'reject',
msg: 'blocked: pubkey is banned.',
};
}
}
return {
id,
action: 'accept',
msg: '',
};
};
export default pubkeyBanPolicy;

View File

@ -0,0 +1,22 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import rateLimitPolicy from './rate-limit-policy.ts';
Deno.test({
name: 'blocks events from IPs that are publishing events too quickly',
fn: async () => {
const opts = { max: 4, databaseUrl: undefined };
const msg = buildInputMessage({ sourceType: 'IP4', sourceInfo: '1.1.1.1', event: buildEvent() });
assertEquals((await rateLimitPolicy(msg, opts)).action, 'accept');
assertEquals((await rateLimitPolicy(msg, opts)).action, 'accept');
assertEquals((await rateLimitPolicy(msg, opts)).action, 'accept');
assertEquals((await rateLimitPolicy(msg, opts)).action, 'accept');
assertEquals((await rateLimitPolicy(msg, opts)).action, 'reject');
assertEquals((await rateLimitPolicy(msg, opts)).action, 'reject');
assertEquals((await rateLimitPolicy(msg, opts)).action, 'reject');
},
sanitizeResources: false,
});

View File

@ -0,0 +1,59 @@
import { Keydb } from '../deps.ts';
import type { Policy } from '../types.ts';
/** Policy options for `rateLimitPolicy`. */
interface RateLimit {
/** How often (ms) to check whether `max` has been exceeded. Default: `60000` (1 minute). */
interval?: number;
/** Max number of requests within the `interval` until the IP is rate-limited. Default: `10`. */
max?: number;
/** List of IP addresses to skip this policy. */
whitelist?: string[];
/** Database connection string. Default: `sqlite:///tmp/strfry-rate-limit-policy.sqlite3` */
databaseUrl?: string;
}
/**
* Rate-limits users by their IP address.
*
* IPs are stored in an SQLite database. If you are running internal services, it's a good idea to at least whitelist `127.0.0.1` etc.
*
* @example
* ```ts
* // Limit to 10 events per second.
* rateLimitPolicy(msg, { max: 10, interval: 60000 });
* ```
*/
const rateLimitPolicy: Policy<RateLimit> = async (msg, opts = {}) => {
const {
interval = 60000,
max = 10,
whitelist = [],
databaseUrl = 'sqlite:///tmp/strfry-rate-limit-policy.sqlite3',
} = opts;
if ((msg.sourceType === 'IP4' || msg.sourceType === 'IP6') && !whitelist.includes(msg.sourceInfo)) {
const db = new Keydb(databaseUrl);
const count = await db.get<number>(msg.sourceInfo) || 0;
await db.set(msg.sourceInfo, count + 1, interval);
if (count >= max) {
return {
id: msg.event.id,
action: 'reject',
msg: 'rate-limited: too many requests',
};
}
}
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
export default rateLimitPolicy;
export type { RateLimit };

View File

@ -0,0 +1,10 @@
import { assertEquals } from '../deps.ts';
import { buildInputMessage } from '../test.ts';
import readOnlyPolicy from './read-only-policy.ts';
Deno.test('always rejects', async () => {
const msg = buildInputMessage();
const result = await readOnlyPolicy(msg);
assertEquals(result.action, 'reject');
});

View File

@ -0,0 +1,10 @@
import type { Policy } from '../types.ts';
/** This policy rejects all messages. */
const readOnlyPolicy: Policy<void> = (msg) => ({
id: msg.event.id,
action: 'reject',
msg: 'blocked: the relay is read-only',
});
export default readOnlyPolicy;

View File

@ -0,0 +1,15 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import regexPolicy from './regex-policy.ts';
Deno.test('blocks banned regular expressions', async () => {
const msg = buildInputMessage({ event: buildEvent({ content: '🔥🔥🔥 https://t.me/spam 我想死' }) });
assertEquals((await regexPolicy(msg)).action, 'accept');
assertEquals((await regexPolicy(msg, /https:\/\/t\.me\/\w+/i)).action, 'reject');
assertEquals((await regexPolicy(msg, /🔥{1,3}/)).action, 'reject');
assertEquals((await regexPolicy(msg, /🔥{4}/)).action, 'accept');
assertEquals((await regexPolicy(msg, /🔥$/)).action, 'accept');
assertEquals((await regexPolicy(msg, /^🔥/)).action, 'reject');
});

View File

@ -0,0 +1,28 @@
import { Policy } from '../types.ts';
/**
* Reject events whose content matches the regex.
*
* @example
* ```ts
* // Ban events matching a regex.
* regexPolicy(msg, /(🟠|🔥|😳)ChtaGPT/i);
* ```
*/
const regexPolicy: Policy<RegExp> = ({ event: { id, content } }, regex) => {
if (regex?.test(content)) {
return {
id,
action: 'reject',
msg: 'blocked: text matches a banned expression.',
};
}
return {
id,
action: 'accept',
msg: '',
};
};
export default regexPolicy;

19
src/policies/replyGuy.ts Normal file
View File

@ -0,0 +1,19 @@
import { Policy } from '../types.ts';
const replyGuy: Policy<RegExp> = ({ event: { id, content } }, regex) => {
if (regex?.test(content)) {
return {
id,
action: 'reject',
msg: 'blocked: Reply Guy',
};
}
return {
id,
action: 'accept',
msg: '',
};
};
export default replyGuy;

View File

@ -0,0 +1,20 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import sizeLimitPolicy from './size-limit-policy.ts';
Deno.test('Size limit policy test', async () => {
// Create a message under 12KB
const smallContent = 'Hello'.repeat(100); // Well under 12KB
const smallMessage = buildInputMessage({ event: buildEvent({ content: smallContent }) });
// Create a message over 12KB
const largeContent = 'Hello'.repeat(2500); // Over 12KB
const largeMessage = buildInputMessage({ event: buildEvent({ content: largeContent }) });
// Test that a small message is accepted
assertEquals((await sizeLimitPolicy(smallMessage)).action, 'accept', 'Small message should be accepted');
// Test that a large message is rejected
assertEquals((await sizeLimitPolicy(largeMessage)).action, 'reject', 'Large message should be rejected');
});

View File

@ -0,0 +1,46 @@
import { Policy } from '../types.ts';
/** Policy options for `sizeLimitPolicy`. */
interface SizeLimitOptions {
/** Maximum size of the message content in bytes. Default: 8KB (8192 bytes) */
maxContentSize?: number;
/** List of excluded event kinds */
excludeKinds?: number[];
}
/**
* Reject events larger than a specified size. Only applies to regular events (kind is less than 10000), as all other kinds are replaceable or ephemeral anyway.
*
* @example
* ```ts
* // Reject events larger than a custom size (15KB) and exclude event kinds [1, 2].
* sizeLimitPolicy(msg, { maxContentSize: 15360, excludeKinds: [1, 2] });
* // Reject events larger than the default size (8KB) and exclude event kinds [3].
* sizeLimitPolicy(msg, { excludeKinds: [3] });
* ```
*/
const sizeLimitPolicy: Policy<SizeLimitOptions> = ({ event: { id, content, kind } }, opts = {}) => {
const {
excludeKinds = [3], // Follows aka Contact Lists (NIP-02)
maxContentSize = 8 * 1024 // Default size limit
} = opts;
// Convert the content into bytes and check its size
const contentSize = new TextEncoder().encode(content).length;
if (contentSize > maxContentSize && !excludeKinds.includes(kind) && kind < 10000) {
return {
id,
action: 'reject',
msg: `blocked: message is too large.`,
};
}
return {
id,
action: 'accept',
msg: '',
};
};
export default sizeLimitPolicy;

128
src/policies/tagPolicy.ts Executable file
View File

@ -0,0 +1,128 @@
import type { Policy } from '../types.ts';
/** Policy options for `hellthreadPolicy`. */
interface Hellthread {
/** Total number of "p" tags a kind 1 note may have before it's rejected. Default: `100` */
limit?: number;
}
/**
* Reject messages that tag too many participants.
*
* This policy is useful to prevent unwanted notifications by limiting the number of "p" tags a kind 1 event may have.
* Only kind 1 events are impacted by this policy, since kind 3 events will commonly exceed this number.
*
* @example
* ```ts
* // Reject events with more than 15 mentions.
* hellthreadPolicy(msg, { limit: 15 });
* ```
*/
const tagPolicy: Policy<Hellthread> = (msg, opts) => {
const limit = 0;
if (msg.event.kind === 4) {
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
if (msg.event.kind === 14) {
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
if (msg.event.kind === 0) {
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
let mastodon: (string | number)[] = ['bae.st','bsky','liberal.city','mastodon.bot','botsin.space','a2mi.social','.au','masto.host','mastodon.online','social.beaware.live','nofan.xyz','mastodon.social','mstdn','mathstodon','universeodon','infosec', 'mastdn', 'kitty.social', 'c.im', '.jp', '.de', '.fr', 'toot', 'mastodon', 'misskey', 'journa.host', 'social', 'eldritchcafe', 'hachyderm', 'kinky.business']
const p = msg.event.tags.filter((tag) => tag[0] === 'p');
const e = msg.event.tags.filter((tag) => tag[0] === 'e');
const g = msg.event.tags.filter((tag) => tag[0] === 'g');
const a = msg.event.tags.filter((tag) => tag[0] === 'a');
const d = msg.event.tags.filter((tag) => tag[0] === 'd');
const h = msg.event.tags.filter((tag) => tag[0] === 'h');
const i = msg.event.tags.filter((tag) => tag[0] === 'i');
const k = msg.event.tags.filter((tag) => tag[0] === 'k');
const l = msg.event.tags.filter((tag) => tag[0] === 'l');
const q = msg.event.tags.filter((tag) => tag[0] === 'q');
const t = msg.event.tags.filter((tag) => tag[0] === 't');
const alt = msg.event.tags.filter((tag) => tag[0] === 'alt');
const content = msg.event.tags.filter((tag) => tag[0] === 'content-warning');
const image = msg.event.tags.filter((tag) => tag[0] === 'image');
const relay = msg.event.tags.filter((tag) => tag[0] === 'relay');
const client = msg.event.tags.filter((tag) => tag[0] === 'client');
const proxy = msg.event.tags.filter((tag) => tag[0] === 'proxy');
const blockheight = msg.event.tags.filter((tag) => tag[0] === 'blockheight');
if (blockheight.length > limit) {
if (blockheight.length > limit) {
return {
id: msg.event.id,
action: 'reject',
msg: `Nostr Bots: BlockHeight`,
};
}
}
if (t.length > limit) {
if (t.length > limit) {
if (t.toString().indexOf('gnostr') > -1) {
return {
id: msg.event.id,
action: 'reject',
msg: `Nostr Bots: gnostr`,
};
}
}
}
if (proxy.length > limit) {
for (let search of mastodon) {
if (proxy.length > limit) {
if(proxy.toString().indexOf('noauthority') > -1 || proxy.toString().indexOf('iddqd') > -1) {
return {
id: msg.event.id,
action: 'accept',
msg: `Fediverse Accept`,
};
}
if (proxy.toString().indexOf(search) > -1) {
return {
id: msg.event.id,
action: 'reject',
msg: `Fediverse Block: ` + search,
};
}
}
}
}
if (client.length > limit || d.length > limit || a.length > limit || h.length > limit || i.length > limit || k.length > limit || l.length > limit || q.length > limit || t.length > limit || alt.length > limit || content.length > limit || image.length > limit || relay.length > limit || g.length > limit || e.length > limit || p.length > limit) {
return {
id: msg.event.id,
action: 'accept',
msg: ``,
};
}
return {
id: msg.event.id,
action: 'reject',
msg: ``,
};
};
export default tagPolicy;
export type { TAG };

View File

@ -0,0 +1,15 @@
import { assertEquals } from '../deps.ts';
import { buildEvent, buildInputMessage } from '../test.ts';
import whitelistPolicy from './whitelist-policy.ts';
Deno.test('allows only whitelisted pubkeys', async () => {
const msgA = buildInputMessage({ event: buildEvent({ pubkey: 'A' }) });
const msgB = buildInputMessage({ event: buildEvent({ pubkey: 'B' }) });
const msgC = buildInputMessage({ event: buildEvent({ pubkey: 'C' }) });
assertEquals((await whitelistPolicy(msgA, [])).action, 'reject');
assertEquals((await whitelistPolicy(msgA, ['A'])).action, 'accept');
assertEquals((await whitelistPolicy(msgC, ['B', 'A'])).action, 'reject');
assertEquals((await whitelistPolicy(msgB, ['B', 'A'])).action, 'accept');
});

View File

@ -0,0 +1,32 @@
import type { IterablePubkeys, Policy } from '../types.ts';
/**
* Allows only the listed pubkeys to post to the relay. All other events are rejected.
* @example
* ```ts
* // Load allowed pubkeys from a text file.
* import { readLines } from 'https://deno.land/std/io/mod.ts';
* const pubkeys = readLines(await Deno.open('pubkeys.txt'));
* const result = await whitelistPolicy(msg, pubkeys);
* ```
*/
const whitelistPolicy: Policy<IterablePubkeys> = async ({ event: { id, pubkey } }, pubkeys = []) => {
for await (const p of pubkeys) {
if (p === pubkey) {
return {
id,
action: 'accept',
msg: '',
};
}
}
return {
id,
action: 'reject',
msg: 'blocked: only certain pubkeys are allowed to post',
};
};
export default whitelistPolicy;

31
src/test.ts Normal file
View File

@ -0,0 +1,31 @@
import type { Event, InputMessage } from './types.ts';
/** Constructs a fake event for tests. */
function buildEvent(attrs: Partial<Event> = {}): Event {
const event: Event = {
kind: 1,
id: '',
content: '',
created_at: 0,
pubkey: '',
sig: '',
tags: [],
};
return Object.assign(event, attrs);
}
/** Constructs a fake strfry input message for tests. */
function buildInputMessage(attrs: Partial<InputMessage> = {}): InputMessage {
const msg = {
event: buildEvent(),
receivedAt: 0,
sourceInfo: '127.0.0.1',
sourceType: 'IP4',
type: 'new',
};
return Object.assign(msg, attrs);
}
export { buildEvent, buildInputMessage };

57
src/types.ts Normal file
View File

@ -0,0 +1,57 @@
/**
* strfry input message from stdin.
*
* https://github.com/hoytech/strfry/blob/master/docs/plugins.md#input-messages
*/
interface InputMessage {
/** Either `new` or `lookback`. */
type: 'new' | 'lookback';
/** The event posted by the client, with all the required fields such as `id`, `pubkey`, etc. */
event: Event;
/** Unix timestamp of when this event was received by the relay. */
receivedAt: number;
/** Where this event came from. Typically will be `IP4` or `IP6`, but in lookback can also be `Import`, `Stream`, or `Sync`. */
sourceType: 'IP4' | 'IP6' | 'Import' | 'Stream' | 'Sync';
/** Specifics of the event's source. Either an IP address or a relay URL (for stream/sync). */
sourceInfo: string;
}
/**
* strfry output message to be printed as JSONL (minified JSON followed by a newline) to stdout.
*
* https://github.com/hoytech/strfry/blob/master/docs/plugins.md#output-messages
*/
interface OutputMessage {
/** The event ID taken from the `event.id` field of the input message. */
id: string;
/** Either `accept`, `reject`, or `shadowReject`. */
action: 'accept' | 'reject' | 'shadowReject';
/** The NIP-20 response message to be sent to the client. Only used for `reject`. */
msg: string;
}
/**
* Nostr event.
*
* https://github.com/nostr-protocol/nips/blob/master/01.md
*/
interface Event<K extends number = number> {
id: string;
sig: string;
kind: K;
tags: string[][];
pubkey: string;
content: string;
created_at: number;
}
/**
* A policy function in this library.
* It accepts an input message, opts, and returns an output message.
*/
type Policy<Opts = unknown> = (msg: InputMessage, opts?: Opts) => Promise<OutputMessage> | OutputMessage;
/** Sync or async iterable of pubkeys, designed for efficiently loading from large files. */
type IterablePubkeys = Iterable<string> | AsyncIterable<string>;
export type { Event, InputMessage, IterablePubkeys, OutputMessage, Policy };

871
strfry-policy.ts Executable file
View File

@ -0,0 +1,871 @@
#!/bin/sh
//bin/true; exec deno run -A "$0" "$@"
import {
antiDuplicationPolicy,
hellthreadPolicy,
pipeline,
readStdin,
keywordPolicy,
writeStdout,
rateLimitPolicy,
pubkeyBanPolicy,
keyworkdPolicy,
regexPolicy,
replyGuy,
whitelistPolicy,
tagPolicy
} from '/opt/strfry/strfry-policies/mod.ts';
for await (const msg of readStdin()) {
const result = await pipeline(msg, [
tagPolicy,
[rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
[replyGuy, /(Let's enjoy the show|Would you like me to summarize the article for you|I'm here to help with any questions or topics you'd like to discuss!|Feel free to come back and ask me anything else if you need help or just want to chat|I cannot create content that is discriminatory in nature|The article appears to be referring|I'm a large language model|ReplyGirl|relay.0xchat.com|cobrafuma.com|relay.damus.io|relay.primal.net|nostr.oxtr.dev|nostr.oxtr.dev|nostr.fmt.wiz.biz|ReplyGuy)/i ],
[regexPolicy, /(#transgender|#trans|#LGBTQ|#LGBTQIA|simpleX|1-1111-1|1-1-1-1|-----END|Sensor data:|"part"|경|TURIZBOT|If you want this to stop|DID YOU MISS ME?|This is a post from|DYNAMITE|#Rogule|MPN:|Author:|#blowjob|ア|サ|#gedanken|#lust|#nude|#sexy|#lingerie|이|#gay|#cock|#cum|#frots|==========|===============|#fiatjaf|리|#perverted|#pissing|#kinky|#fetish|ミ|今|シ|ロ|タ|ワ|일|#gnostr|ヤ|メ|コ|中|#regexle|自|ゥ|フ|月|ĝ|手|川|ぁ|什|マ|ル|ブ|ぺ|ぇ|円|万|キ|ャ|り|予|ㅋ|어|할|음|디|지|で||ス|ミ|ぐ|え|フ|ラ|れ|ゴ|タ|キ|ク|イ|ก|น|พื่|ド|モ|ふ|む|theYescoin_bot|休|ン|ァ|#gnostr|二|を|下|出|門|へ|ぱ|し|首|大|子|소|트|든|소|#Worldle|아|대|인|百|合|東|ふ|む|牛|跨|界|บ|งั|ぴ|よ|ね|ウ|ボ|ァ|沖|田|#decreasingfees|"ping"|常|可|非|#metazooa|ã|ú|#WhereTaken|#Lingule|き|チ||จั|ลั|ม|ü|ส|น่|说|胡|#Swarm_to_Nostr|ą|ę|Hello World!|#pastpuzzle|#Polygonle|上|几|哈|#waffle|ぃ|ひ|ค่|め|こ|わ|女|头|É|#Photography|#Bicycling|diningandcooking|botsin|lume|克|ッ|ー|ア|か|示|来|ç|à|に|う|ろ|早|ハ|だ|ガ|#Horoscope|ハ|オ|ヨ|#feesbelow10|#feesbelow20|utxo|õ|た|い|お|í|じ|ま|ご|europesays|元|不|一|replicatr|varishangout|9gag|✄|屁|〜|良|す|る|了|#caturday|人|#Ukraine|#labor|ぽ|ゆ|林|黑|Wordle|ć|ś|#Bot|ñ|ん|#Airport|も|yadio|SELLEUR|ö|ä|н|ч|и|п|д|ê|á|สุ|ด|는|보|trojan|siam|は|ン|ス|ッ|nhk|っ|ー|の|て|サ|ة|ك|หิ|india|あ|と|rss|eth.limo|vmess|ク|ょ|日|立|ら|リ|nsfw|loli|shota|#ass|#pussy|#reddit|#lewd|waifu|hentai|#porn|telegra.ph|hindu|재|서|다|한|로)/i],
[keywordPolicy, ['https://media.channels.im','pay rent','Revealing nonce:','NostrDice','https://media.infosec.exchange','npub1q6ps7m94jfdastx2tx76sj8sq4nxdhlsgmzns2tr4xt6ydx6grzspm0kxr','My goals:','TURDISMO','https://files.thicc.horse','https://fe.upload.disroot.org','https://media.beige.party','https://s3.simkey.net','https://media.me.dm','https://media.fops.cloud','https://unfufadoo.net','https://t.co','https://witter.cz','https://static.theblower.au','https://minidisc.tokyo','https://cdn.noods.fun','https://thumbsnap.com','https://paste.gmem.ca','https://x.com','https://assets.fedeaated.press','https://media.outerheaven.club','https://kneel.before.dog','https://media.nofan.xyz','https://media.horrorhub.club','https://stockroom.wandering.shop','https://media.troet.cafe','https://ovo.wxw.media','https://files.girino.org','https://media.absturztau.be','https://files.ioc.exchange','https://media.cmx.edu.kg','https://media.suya.place','https://media.valkyrie.world','https://voskeyfiles.icalo.net','https://ak.kyaruc.moe','https://mast-files.shitpostbot.com','https://cdn.aethy.com','https://media.spinster.xyz','https://img.pawoo.net','https://amami.paradigm-x.tokyo','https://social.mikutter.hachune.net','https://imouto.pics','https://pl.slash.cl','https://matrix.rocks','https://quark.scrolller.com','https://cum.salon/media','https://www.manyvids.com','https://fansly.com','https://onlyfans.com','https://proton.scrolller.com','https://pomf2.lain.la','https://media.wolfgirl.bar','https://api-test.summary.news','https://fans.ly','https://figure.game','https://gearlandia.haus/media','https://link.storjshare.io','https://kinkyelephant.com','https://cdn77.scoreuniverse.com','https://milker.cafe','https://open.spotify.com','https://media.gameliberty.club','https://media.baraag.net','https://t.me/','https://nyc3.digitaloceanspaces.com','https://images.scrolller.com/femto']],
[pubkeyBanPolicy, [
'1beaf8643e4607f73dc44873ebdf84d87598aeed27a84ecc351d253b6bfa1794',
'c231760b10cefbfc3d7bae5e2d5b40e2ee1714ff90cb78fcff40ba82122dd2be',
'00c4cc916e1a9944c491b25baaf133f1819beaade4a7eee6eff4a6bf71e3ed17',
'3dc04334f758bea5a82f896f7670579407b49b1a9f3294d3edeec6d1e9c25673',
'6c86b60b38bfa4db973a000769515bdcba5f6ab81ffe1056e98a7c8771a1fc43',
'2ecf1b64caf12aef69e46290b35e4742fba4dc2a479a8210c355ad4e9780044c',
'f440ac8aeadb21b1c7084c169643ff52a5fdadb1524cd8153cb7e8719ab55bcc',
'e16a709785f34cb55c4e3e0d282ec32417995b768dff889174a205929eba330a',
'3170bbd152ea39e8116098c8de411358692cf15c5d3bed58d2be8fa70849bf1c',
'0a96c51c0ca412c116c04f9bbb3c1338b1b19385e123682def6e7e803fdbf160',
'f14f7e19d44d51428af6082c3833a2fa323c04c13d8447a518d4f22f449de4a4',
'be848639eed4f1a2daf25cee960c14e0594e37b9c5dc80fad970a21030357c62',
'b0f4c26e4683c34fe8a167c3098ffb6da1df78984a35e2d79b91faf0c546020d',
'1127f7f68a7ab16a42993fc31ed05a91c90ffaf69009e62676d2608b6090e1c5',
'8b928bf75edb4ddffe2800557ffe7e5e2b07c5d5102f97d1955f921585938201',
'cfb5151d9a4242a2a5c1457bd8bd75181b49dcd8e2d99c3b5c2e8ae21f2f0e79',
'e1fb933387600b254f46aeba3d92817db922bcd55a2ae234a3aff636c9a77754',
'f41952a0db4f1f4d41fe93385747104b3afc8c4939d8f5d7afc92a94a40372d9',
'21199c8bf5b2a3d1a2c5e77deb835630f4648bc0f4326242b39b967f2359f41a',
'67ec4aad46f716fa19c29d7a39cbfb392cf83373f77fa5ce342f1fe803e5c798',
'a047dd6e197529d3a2339fd03be826292ee531dc7cb63920051807da51fa05af',
'900230e8350ef8e110897243056e1c0b4a6cf970dc2d88bcaf25d347ed96dd0d',
'88bffbcfdc8241ec1caf619f2fddec1e64961c9bea82509d64a47530b6df9d47',
'965e47f6980518805e36f3d696e1dd22e0466f495d5172e1ea4e20735c018be3',
'2bce73c6c288c17c84bbabd0cfa91be6d1ccd55de3055724e6fe659b5dbd536c',
'dbac778388545a63927fb914677b3c866745a00e39a626d4d8bf9b249171a9ea',
'064addd9ed32d23eb4582a6a975556671733fda676518beafd8a9b5b60d25297',
'38b78743026c829d04f987c987ae35e735557fea71d4c11242e746a56087fa5b',
'5ffae59b6cd8535bec9b10b8ec2291cf9a91335e1d66b0aa110d3a69875e852f',
'850f47487c0ee135be1eed0ccab8d5f26894458880d33f7fff1d45420025d752',
'7c3be2769c76eecd6c6e27276722dfae0d8ad201825253452153b90093c41654',
'e871f428b08722109e0dab3da66f6e38eb0c2cc54ee98315170ca34b82342245',
'952a8c7ac0e7d13ef505957d547bbfbe86c93e0f79d843c5ec26f1d8fb2aaaaa',
'eb45a44fcc0e930ad548844498ce1827f41ebff9b26a05dc3825ddc2c142a481',
'c231760b10cefbfc3d7bae5e2d5b40e2ee1714ff90cb78fcff40ba82122dd2be',
'5ab22f9bce1b00d157d5057495ad8e6ed2f1d95874a2946a822dae1ed5659a03',
'ea3efe2c12a4660477d5c01ebbca57a725da2ccc281b9d9687ff3452ab8c8f03',
'92985c7bc455ea32474b041397b2ed6fcd9356d568eabc222b94a031354b5ff2',
'de0215acde4d0eb51d39bc1553f137eed2a2a6b4c0c2cd4f81310e770fdf99f6',
'de0215acde4d0eb51d39bc1553f137eed2a2a6b4c0c2cd4f81310e770fdf99f6',
'c159a66ba8904f9c721dbf1fbe6007e1469ab11166278e5b78746436f5148957',
'73dc8f9d9a8f3d741858e76af2fad90e5dd803bc78da04ffaa3ec602cf12e15d',
'c1ea75ed80a002d6b0c1c242ab0b9b6e4c2f11656cd4856da24d623216c7dcc4',
'cd1ac0694c020fdb239bc5373a63ee8afa2249e83c4cf73ff2d37bade85168c5',
'88e7c45d517bac4fca07c0970c3ebb048e2903012cef394b11d36e41971553c6',
'6d92bf865038c0fcefedafad079a70b0167c3b3577aa3c64ab21e33e03fee970',
'bc0a23162548883c082c592d4d87bf9dba283585d9a6c735f1069fe8d52aaa46',
'f41f9a992f296fcc78f0b06cad782cea5e3dee943baad8be39a79fce93a1fe8e',
'0750bec5c3243e8b873b81f6c564366ae8a71a200c7bfe708391fd77ad728e6c',
'a65905be2748773af88e1e14c3dff4a57112474ee4a9eb9fc3dd343e20a4f287',
'bd0a37d9c45e54e5665937ed90efdae0dfb5bd7cfa60e13e0d6dbc0f9a9e42b5',
'0750bec5c3243e8b873b81f6c564366ae8a71a200c7bfe708391fd77ad728e6c',
'6cf8544fd32a110635e3f4c9fc4fec438343effeff2556f281f7887b6a1980b6',
'7383de56d9c762ba6e11fd41f6a0028de846dadd97387cca91b121f45584ef36',
'74f31a39cc65da2b3f5daccc5178a261c6a1945483d03c3a8b416a20db5c7fcc',
'45a1f2b3f8cf99a3f434ae065151cd0c9dbd63a5669ada0fb65d9864607ec998',
'408436d097dd79ce35d73d232a6e05bd80bbbcc6c1bb98a4bb76538cec117c95',
'113948a7b7b5e63d86c5beb5f84edc3da9ed5a00192a9700b63e0b7398cd51ff',
'b9c1f457d3c9b65fec71e0f3c66a26fd2a7b99143735eecb49da914739707a8a',
'5c383c9db3368b8ec369b553a708e94e12daa947261e8b91a2f595d1e8b2cf50',
'31bc7fe353174b86877f970b900cfc846104fc87dfbb2e5ff3ba209bfae69398',
'31bc7fe353174b86877f970b900cfc846104fc87dfbb2e5ff3ba209bfae69398',
'abe08beb4d1847c778a95304c4ce4afaf302c9c85d5c26aac108d0f0409479b0',
'abe08beb4d1847c778a95304c4ce4afaf302c9c85d5c26aac108d0f0409479b0',
'448213a1537860c51be55b8f5af3e06d382b918f15502ec536c3698ac748f5ab',
'448213a1537860c51be55b8f5af3e06d382b918f15502ec536c3698ac748f5ab',
'596d6273f097f48ec9234195a7165f6a54b1cbfb0153e245cf5ef050c06e48e4',
'95e827f0656f79eb83384c30ec8418193caee3b92fb745d666a272ffad769e48',
'ce7d547875f3cdc95c7d42fc7949737b8f3341348de02457eeec401f15a99d6a',
'c94cb21f2e753dd8cc2c5313fa633fa6bd86bed582c1c397bdd7dfc9b6cf08cf',
'd17fc0ee033d1a67b5e0dd9bf2f3cb49a45f00d22bc2c6598f8473cc9dc9a585',
'4145da63a7c5c3c2bf36963e825cf94ad47c96e93434be8eea766913802efdd9',
'1a58d81c4aa659eeabf46bedb2eef91784a220a09ec6e548d7a59250d759bb78',
'20a70367e66591c1a1ee866e0e8df0d7d9522cccef6cf07d5c50e7453681e4a6',
'd86c9e94f58a4ec346f80c552c17b4878fb5e2894f670750afc08b1fac9bca1d',
'edc0bf32cb5d06a6bf89dacfd424f6d21ceeb3f7f9b26a32d6a4212b513de970',
'fcc24b1f5028af2acc5bd630d7df86133edc9393711b9f02c40e2c0289aa1c3c',
'ae8f998a16ac2558237ae449b8aca6848c3d518906543cf9d51104c58ccc95d4',
'7153c4c819e6256d13e846894523faa919013e24e1a089f40f2857bed5ac8a1a',
'79eebc9ed8fa0069007b43c79db451fbf4e5ec6a73cbf5d3b835e592fedeec65',
'22e5d2bca676f065acb6872b6522be27b83d85d65151fdad396db3ca1e12cd42',
'35eb8a4bb92f12832e24e9f59d54271d94cd195020eab82f51187c8e78a0c88f',
'9d47b94f00299ab2bf7c1ab23da00899d4e06eabfc2b6313d62cbc286d353de6',
'16d114303d8203115918ca34a220e925c022c09168175a5ace5e9f3b61640947',
'4965080cd7466132e44e6b03200683c5685453ebf9226219030cb60a08cc485b',
'87570fcf0dcab7fe4f5f128c73bad78a2bcdb8188a62c37d0e31341d1d9a3b69',
'd54fe39c5566b47deff048bfc81394012048e4c82daed46397a70bdf4dfb75f9',
'd54fe39c5566b47deff048bfc81394012048e4c82daed46397a70bdf4dfb75f9',
'4a4a831d918184b648e82bd94f0ecbc3a1768f0a6279bdc5f4256b687ec1966c',
'e7bf8dad360828f0289b7b4bea1a1bd28eb6d4d6522fa17f957e0dfb839ef3db',
'618918f848301edee77f21faa3f33860a9c5a8fd03729af40c18023f7da701b8',
'be45b217db28714a78366b4b2bffce03c63931fde5837148d3c6ae2f8deacd5e',
'be19e0b22cb7d6525b3d664fec8de3ccc3fe535b18b76d1ede52f354cee90a66',
'053c429e8cc745731fd3eca80bb48636d63183b6483dfdb2e60858de0029b660',
'7e30a5d7224ff44c5efa7415eb6a830bc8a84e527e69a61540b376cde62edc64',
'930d5fbf4bba8a281012129a348baf049115da05f750dac96dce9d6306e910eb',
'85eac2b7f3d4974279a3c32897b0427b421fee179d8a740d9c1b2ab9af284d3c',
'c0765dae45d9483a6f180e4c079b98befa320d17d08bdc06e87c505bee4ea59a',
'310ad1d5cec7a61f22129c887f3b6ef4d26cc57fdbcd4183f9071cb40b114eb4',
'310ad1d5cec7a61f22129c887f3b6ef4d26cc57fdbcd4183f9071cb40b114eb4',
'446e48b4737a45c6503cc13e679c7c91acc58d4cbc8c01056a59895b1ec7ea52',
'446e48b4737a45c6503cc13e679c7c91acc58d4cbc8c01056a59895b1ec7ea52',
'4fb4d8dae1d3cd034d393818425c76e41e2818055087a51c19a87b4868461370',
'1a71d747052b062734b2a1bdc3dc0cf4ca34041569c5485b8b615defa7ad0a1f',
'edbebe98207e6ec16a9ba2305d2d6ed58704e9515121a011dda56363ffad9f69',
'34f6436a9361636e398babec42b8c1562a2f72682981e0f480ed874517d4b75f',
'74804b8ba4a0f0a28613d74772e05cfca5aebc09e36129a920b0e26b7df61630',
'a049eada0725b816a5e73afa158fa178f1d396b6a3130c725f24f27fd547278a',
'1a71d747052b062734b2a1bdc3dc0cf4ca34041569c5485b8b615defa7ad0a1f',
'c85d8c76e09b417866fa03d50cf2afc03aa45ed1e156ce1544bd6edd7f664463',
'f23c0bff95c772969ceceaf396ecaeef0a4868cbd4af2c278ab008e29b73e60d',
'053c429e8cc745731fd3eca80bb48636d63183b6483dfdb2e60858de0029b660',
'966cf573a29c4becda1970ad4d8e05e96c666c81705dfb0b3eeee3e44f9c50c8',
'a30816b063c858965f032ee5aa50b6e8091225e583970c2b167ac00de6c54ba5',
'966cf573a29c4becda1970ad4d8e05e96c666c81705dfb0b3eeee3e44f9c50c8',
'cf94884ef3330842f55faeeeec6bb3b0e3f0c63ccf2b9ac5f15c94752f637cda',
'e547f88be9eab29343f72053566867676997e0b0d9883a6bda09ac4b3f179c63',
'6bc77329e2add280c880ddcefe2e39b7f5e1e7180aefa7fc47f5d8132a358f3c',
'0493907a4cbb606239c2bf3334de3f21c8ac20686dee0a40d9f805a8517e9d6b',
'd84058dec4990732b58e07cb063e406b796b0938e14d36d70c648cbae2067df5',
'4f44ff626cb4761bcba7451261b8ff35b798d798e12474d5aaad8ce9516ae4ca',
'9d51218e8c7868b3710936e22ae5c2e419378175ad3559d28a1db74ea0c80d65',
'c3b4ad337ff8859cca9638b370cef2cb30782c185c16d83b7f983a7a18cf425f',
'0e77895ad330bce71dbc7eb77102a6f1066a97dd4713a8822e3fc9fe9e160433',
'2e87620c737a2018d5fbac8fded2c2f05f29f8b098384d7d1bd1f93aa3875773',
'f9bfebb47ac8ee357eb81c79f9453f6fb32a1086df3fefb3d63bb17e754da9a8',
'bfd121a37773554f2ef3e8e524e0e337f3bf852eb55cd9664e2c901514f063e7',
'008d571012e0dbb5b4ac6df988900e0fc4befc74f7db1c51e7512af36efab7a7',
'34e21ea013dfc435e8eff38171443edc8a5fd416d41e04df59391e0dca456df0',
'8757018160583146bbb198a919716ced04143112348da67f3df5e97d8e95f5f9',
'1e3aa9756736f8eabd7e37b985003fe809c7df1efa9779d0ce49aa1d80324f5f',
'4c522cb776898f3d81ecb65335c7301a11166c93bbcbae4b4a723b34d6aa0e9c',
'd5be648b8281b16334cb4c92e9849b0f49a27244c034f55e9644f8230f4e6a51',
'f71d75078678351394914ba46d8f98aae05c6e325648b4199d4352825ebb1023',
'e2a344ab03271bd7683f0c120b553927c4c69531d71284904365bbcb0486a056',
'45281bf8fcd0d1b385e633bfa13938aa5f6195e0c9b09eda263d9d82929f3f8e',
'45281bf8fcd0d1b385e633bfa13938aa5f6195e0c9b09eda263d9d82929f3f8e',
'5063a6bfdd32aaa37b68cfa750463823b94b4df75b6afb9567204dd328181fbb',
'c3a746a08cd5dcf0cd31fbb245fe285202fdda1ae3a16ae636c8d937cd73f4e0',
'2118c453cf96a02e9c8213e0b3e014c77c5aa1650fa665bb4e784d4a4ebdfc39',
'b67890e5804302586fa29f7c4b98e1e73e4208d7abfe642056908cc22f8b5b52',
'8dd6c501f3b84153f5ae604634e160528cff6781c1524042b2c7d7df0b47790b',
'e3760941f9a18bea03a6a1e7418fdad22fceac9797225335aeaa5b2c07632cd5',
'5f8cbe4f487d8ee6359f87ddb6e4acf36584d2a03b353a54247dac4a04950394',
'84668d4e868be589a88a9302193b1b21008c130f40c80c85b01133a6837eeb42',
'8dd6c501f3b84153f5ae604634e160528cff6781c1524042b2c7d7df0b47790b',
'c2787e6a108b299b5d02734e0e2a377b20d5330b98dd4e5c1de018d03cb74396',
'c231760b10cefbfc3d7bae5e2d5b40e2ee1714ff90cb78fcff40ba82122dd2be',
'f3ddbbf7c5bb1a0f79767a65671ecf7a2e3d41e9be0004a08d4fb308fb3fd1a4',
'31fd4a5c421880da9983ad0ae92fb3929fe07136bd00edbf0eb188b813a20eda',
'a6f8a7e2b94c14468df8ba47797dff75127b640181f495de2d17e79e2111e8b5',
'faa5e4554cc1cbf7b57bb6616585bc1c73011ca443c81e57c18a874a90be9b8d',
'fc8314cbf4437c205e111c449dc5292e3f2b3c79be1ef5a74d05db38e4be35d1',
'7a4e2ae6ce6584ef40f0f6f315345474f82b8de822584592a4aab60044963aa0',
'583ef554f95ac3c34d18d90005f80788bb8357b232236646815f4826c66e54a6',
'6b63979899b088cce9ba4555d26acb2a7aeacc00a7bf23c76b771bdb50b4180a',
'acff7e97db3947ea02c5e67aaea46ececaf8291138a4f7a79a50029ca58bb6bc',
'b8de67a365b436ce300eb1049973026143f99e3d3712b326f302b7d6f49a2902',
'f584273ca1f41f907fc285260dc2a5035a1712a8d5405deb88828fc189dd20f1',
'1797a5a14e80071415e41ec0709875fe024a291a6d29fbb52d63b2715a5419a0',
'ac1a1aef6daa0379cb0fb2839dbc91e65ce6a7c90ba1684346705bb2af5bed71',
'2bfd4e836c510d5a86ea4e89fff93995fbcdbb6dbe8c6c4133dcecce7f824141',
'60992cb59e60f4c9a9471c040a15da0a0593841ad19a8302f8fbde18ad4b20c1',
'cdf679dca39eb2adee97f9c2386320d0262422a401c5959fa2ee7c444c858b0f',
'979a28fa43702f9be4e468836a5b120cc4265237f4295fcb4a9b28e2a71d1c6b',
'bb062579e3b3f3a3b395c496db235381cdbd65c790314452319e1ab6dfdce514',
'8740a2731ff9fa1c3aa0c2e0f9fa053c2f2272b2fdf325e045584a7da9fe8ff0',
'666e61585aaa5cb10b94c266d39f99f3d5bc3ecd6434e829fe8360b0ee3f20cc',
'9ce5b37f4458ebfc42083b61bd63e23f7e679422a8e67bb4aa227e8585bcd34f',
'f163685e46106bbb781286525558e065f1c05bbd5846240bcd7d8735e04ce115',
'14147765a18028fdb386db2891e747c11b2a32761e12a0e6e43d5f8451ee74bc',
'000ea9a23ecb3b916a05e1fb9e4e92c46cb38d28b638e038ad8103e7bac542da',
'a6467b359a599811bf9f8db6fa8498f19219d22c03cbc77494d6e00b0ff136e9',
'0b674372b1267e295b293c83eaf149235341196138b3c99dd6e846047c664b15',
'cb5c60a20cc546dedbfa06e1a8ec46f75e49cee16199ec4be9616e807657d247',
'ce5fbfb4a5f2c7ad6ca8c0399be0744ff1e8ed67ceb00593e010980f4f6d0e46',
'3b90a8348d688f79312d40fb1e5fdb02091153943ade9ae3ce240259096a5679',
'ab19f56f8469b0a1ca9214f875e44dbe9f3f6a87687847a491039e11d23b6be6',
'99cad58e15e35009e8b23e58909a0955bc4a6553f399b5cb1b1f05eeb04e607c',
'd1a6c02d18d0b7218e0910284da1696b1ffda1a71987aea6c8581144f0780daf',
'19a9bb533cded2dc59cb9acd3d9ebb8fce883b789f4bd0eac59ab3ed8d8d4256',
'6a8985be76dcb69ac3a75f4a3d4bf393036d5dab57fe3b91b255b37333f993ab',
'24dd0d8580a827c489b514b484274998488dd9c28237cf2a48bc02fc6de62dc4',
'fbedf5a9de23cef8438e6e0ab4fcad05465d02070bfaf6b04e7bc75212c50cc8',
'fbedf5a9de23cef8438e6e0ab4fcad05465d02070bfaf6b04e7bc75212c50cc8',
'88d7780045bd28cb3da3daf75de29c7b4b3794afaab8e164dc84acf1def8e015',
'd10fac1f73c76fee1441bdcefa2a737bd2afaa1489b3cf812c1e2e9550262ee9',
'f3b48f55ef996dc82bbb6ed846fd48a0232a074295bf525137a3f90552ed1367',
'50554dcc16e4e77e05f0b6ac47b18b05033b7439118f6be8e56b14cfa9d30a98',
'4f129b892cf19a3dd2eb6e6bb097349e88e9fb51c035b119795900c8235ab5bb',
'3bf295645b0c31dea13e6e702320f33c1d77cc6e84a271242b7380385bf1f22b',
'ca9fad8ad020c9b1dfe41ce3f404392a59d3173a159894e7b40888427b693a09',
'fbccbb50aba609e001d2ad08ebbf39b085afe939a61d9379b92e8d6c3f75103a',
'3dcae08b0e7437a1b13dc9e74c8fabda0f3e1559e5aa17fc5929d01028371a40',
'fbc58f200dce77ce1ff49fdfbec29f125dac4d7ff49a95cc39c743cf68c9ca53',
'96bbfb2589ccf7f6f943fef9640a5fe9c2585190a4e39325de88d32aa41aecf6',
'66f9eb18d95f392bbfe1c843608d0d9dc98301139e3037ff7dcf2bf83f49dac9',
'21cad8f57bc674ad1ff487abce73445ee0a822d03e154c0d075f15f8f3c8680e',
'df24b338442b1d3fc892b3fe64f1364170965f11e14cf488cd9aca72f409ec1f',
'a1114e302ea5d078da6b83ef581ea03ce8828f1d80d9b28689c2f99c88cee183',
'03fae48550ad960885282f0eb9b09f8151310cb8bc443ae550605bd28d3de5d6',
'2da78dcbe9dbd27124367a5b3f71d2f6aab7850cb259966e3b26a5c6c30cb73b',
'7c1275100990fd7cfb13b37729619c07eab68ff07ba58e6c28bbdd613c398c42',
'9a5978183812c3fbac88ef46c7438ff48e59ed59104457c0d105273a5e921d4d',
'3dafc5305404e17c8c7adf8c5285e27dcd48e86209037ad99ae7d23cd0aa1929',
'6a8693dbb5cfaf6d49ca7149fbd2c6cda13033e4649118d7934776df53c86ac6',
'd4fff26b491be1bb8b2a2721106fda628f945e76d599b0e24454b5dc5274278c',
'13314abb7acb5a471f5df118e594ff91db4e0cf03567771acfedaf5200f55b96',
'6d30bbc96c99027f6bffe8151700f650f5c0be0cba2d2ac814dcccaf5cb99d46',
'a7d6907cec023a60973f78ee17968d6cccef0e2ff6c141e75167141340d618d5',
'e2f96e8c533666fe75d12ee7b7901232195370e70b0ab5293affabd308b1060e',
'033f531776823e99163ff8d78028fec0faa1e580aab4372281db68cf808a8c9b',
'c0bc602ead82fdb644eb1338b4e67fad675cc6b948dd427e555561987cb1ccd5',
'9469657c99868d5a424a99190de524cead5cc3837074e9c791dac2bd1214e1c8',
'875703875de60f883d0de4f4a6544a72832b3211fdfba6f2a91299a0467eb84c',
'6a4c0d5962f6ece3dd5ed04b78d8daf7143bd7630854e96b0173c296f2772cc6',
'66bfb6ef55939953089cb96027aebbda176a6a795dee57e17a7d96ec04f67714',
'6f52f93a7b0d238431d48ebf3bd33e062bd98330ce09f6f3d14edf9a56d8c133',
'bf2b0877a6074ae9a3a030282dd32b298851157c9095c02d9b77a8da58db9c9f',
'627d26021526b550fd91d71c9921fae6ed645a0076625568f4e6dd74b39fd2e5',
'1e5b59dee6185e826fc26d74da80533e682699e2430999521dd87d1bff5cfd8f',
'3493b0c2376fe2a91dc804985ffd681f50210611597a2003f724c6744ce7120f',
'c558c7cc69bbda3c271782b736babc64acd2da258b14f356dbca966cb0b7b89e',
'3bbe8cd47729276f1931ec64cb1b50f4e5d1a5377862cccdf14ae0e50712d40e',
'6079fa0da56d91e29da08829edac8f497bc8eb0013b7bdd79b08846ef12314b6',
'9326e9820911ec0d04fbbfdc63e368e2a62712deb14babcf283e77723f076058',
'8244f39d35ec71b3e2b69a98d07a8cf75d3cbea5943119af0f46801ea7da9c22',
'8162b2bcf003aac73dc046c4d3f9e549d96c1a9b171ad7ab8e2546bd403a9b93',
'ed77beff38bc845e1bf96003c99f5bf37b90f4792442fd369e45ab70c31007fc',
'90f3ecede29c5f2e801702802d4b49db88813f3b13e393c2f08c59eddc264cd1',
'5734dccfd0b9240dc4c4f0cfe99cd34bbb26e7e9a54af891f6f15cefe9b167e3',
'f4a0cc0534dfbec70465eba949194bd5ed4b691322239a4dd3c61f92e97f2923',
'ac7ee1c8dcf261fb853a4ae7451c19ae987dcf3ecfc587a19d6d6a9b8681b732',
'7cd5abf283b30add19c10b830dfe5d609c98cb7eae6fec8158997890fdea52ac',
'0d06480b0c6e3be3c9a1a65d7e6bc2091227d55bf4c77eeb6037ba7776c300ec',
'b0f555f5a343265e72ccaa7b3bbaa7f62024f2e26b0da094a54fb8a264ce89aa',
'f25619d96032974db5d5338c9228cdba320863fe09df14fd7dc80c1e13c44b08',
'50c67e07952f655005cc8032c37af33ed1f2d36be957f6c45dfc98eb882edc03',
'505c52e214eea5a1d344ea4cb8d2923de27bddda4de2277277914bb8a1919a5b',
'd13dbe864435f4e15f20f8bdf5f60b26e544e91c821d009c79da4b1bf781c653',
'eb8d73d31477612d3db55719b1705d57192e89292efd0b214c4f8ca49d04b3ac',
'bd9235e4cb2a2e32937e631a2805da54fdefcd8014e9e8955111fa4bc79db68e',
'65dfefde2efd179f631c4ad5b088cdd9de2b557410368b8d15e2faebf5af725c',
'7152c085e88025947507e8c2a70b5711c409176c9d15f5a2948b12ec0f204fab',
'6789a15735d4ac7cb13396ac59a0bdc813680111b9df4a7ff2497a364c869fb9',
'b982780a1693342800395afddfb152252b680e8805f18c2bfc3c9b3ddffb69c3',
'957dd3687817abb53e01635fb4fc1c029c2cd49202ec82f416ec240601b371d8',
'ac571314b5b1ae8f98d34c9d26d43b10656fc38e6933105d0f1bf12ac9ab052d',
'0e091be73aa5178c87ddad13b8f15681586b2a2154e4d74937ed828e455d27b1',
'decf2ae424d7e66f49a8377e573b0fd38e5209f5d895571ab6dc0caf307383ae',
'c13753c93a023affddc001542f680ebb57a133e075d8322a0d1802c70c383313',
'dac474eba95007f7ba86674771eb5232f179a2e4ba4516ff602b74beacca53da',
'd9e6b65342256051ce6461f7c68b64b0fe87be76dd355c89e462c74505e623b1',
'2e170294e0247c9a14214116937d87a6e5726e46cd3aa4abe1cfc08b41e42f68',
'8f1b628ef24c83619e12a8387d8d91e74e20be204fb94624600ad4e0b916959b',
'd8ff6e50a6c2ebf09d847ec8d8ebbf9ec2252b52f9a1394f3969bf297465ee89',
'd666584a866e1e0c2afa25144a7b5064c3f45ebb72b20edcc14fb35d93465802',
'949776df3ab4550894690ef68363a92356e7d622f4b71326325e3a62da5f2d0d',
'd3459bca5cc479795e87384868de533256c22dffe43487e226a7ded8648beeb7',
'4e82af27199b037ab7e8886b15cd2e051a2638fd25c42f85fe3e212cce605307',
'd981591e0ea6153b8687b2aed670ab7d9b6c3ad018a360b2820b3cf0f7c0ae37',
'd981591e0ea6153b8687b2aed670ab7d9b6c3ad018a360b2820b3cf0f7c0ae37',
'bf90225fc752f2f0d86d566a7b2e15f8beb47dbd423eef8f4953ffcc109dce3b',
'6684d96fc2877a766e0ca6bd1009829db3bb9d4448a9eaa2c3517fa316a192c6',
'6513e30086c44e0ad7a1fad4a8042c14c7c7066ca5e3b6d84b1379d0b4e5982a',
'5113b58718bbbe7985ba5e88938cf33f87fd69a296a45ac6dea41e546714a85a',
'd2fc0bd27d68adb5bc038976538d9110153b1d8409b2822e5a3b8b7f309626c3',
'351a6cf87baa8dd984f546fce2e327bc8afeefc86b582647b480f8f6fee4b7ce',
'2b7133239f8daf3b09fe4c11bb6bdb6b29f719410f60a776b6b2bf50a432a739',
'd67eecc8f2aa87cb976908a5c47f7d7defa805a726973e370f0e455e0127ddb8',
'e96bae739960f32896913145780360ab68d9e11f9e7026ac165dbca73f43b2f1',
'dfb795e024682d8effc33e7e22c5222643a695cccd1f1e54eee7d8ec83043972',
'b2ccb68eacd2c1ab5fa940e85e3e2c738f40daec40f7a40974481ff9b84ebbaa',
'c054332e25ef1ca0feebcd580622613bed81fec31e41f52f27a644c993cd67ae',
'a7a83252b414733079dc03e2eceb6b18975a93489cd0692393cc872e43399da2',
'e5f3339e34bf2f16dd03c62cad6bad8a4d27a9b776f37d65115983e20952d54d',
'870006a719936d5c44aed23e176f1ed12d1bc50197f49d9ab2a2d12fba2afa9f',
'beec1f8ff8761552b39d893f513c6becd41e6fe4f7ae4cf2c926cde56e08d746',
'b64513fc2f5ddf68fa9e279fbf218e6bd95ea8a5db8c2b5c4b769d578cd0cef9',
'dea4c57ee5830c51682cbef5c3a6c6fa9cb0299be337ac41a8488190e4561cb5',
'0f4e9f2401d723285630496323105aeda251a006488e967f0acfc851decd1bb2',
'a7deada843d7cb0d248d200cefb048300f3094d1d9e75d25b9803cb3f1514e4a',
'731200533c53ae2dfb70c3980cf395023f6c1bc687cefc12f67f9ca8f75bdc86',
'23610aeeb02c44c16e57655a1d1592a9dd8d2cfc498e312d6c530118bc832a32',
'08550ea11964b34654db90ebef55a39a029c1a4f0153feba7a41af8fd0bbccc2',
'47f9425eb4da30188ecb402b9c2b2a49cb2949e21d05a05093926866e98ee1ed',
'1ae011cb34999af15602a2aa927f32bb92d65f6161ed71abdaa4f50c1257a556',
'80e05fa8393693bc9a8ab77c40d8fb82916afba1ba1601ff44e53e43dc484280',
'9930da7eeb4ddeaf7fd3016b542743bcf2fcfb24a54f1bceb84fdb927962b24f',
'52e8de56148763f94f0a0eaf194f12fcd0fb275131ec5a15ae91ff600139e3a1',
'4ef1af4bdc4c39012fde27805055ddda9c3e02aeff195f6c078445166f6dfe87',
'074e77850a522806ca278bc6e42a35124deac81334dc7a3980685a3103be8717',
'46c3121ad061c79fa437fad25595c10f453ae1e19dbd6616f9bd26ab295c44ab',
'7539e0d2d804fb0bef6f8363d49781a7ec395a8bdbb385960c017821ed35dade',
'b5974375791fb9946a41c2d13f4893bb23ff9aab23843b86a8a6c87b84379b21',
'78e5eed81944d3cd0676242e18af1f0c73c68018880514c67c31adadd47be596',
'6dd9c2aa134dbf245963fcf6b527286e5b5f9ab9ca22ea5b0e7d855fd92db20c',
'3ec1ca51c6a8b021e62ea1db92c35b0752914baa24d8addbe4e46d34d5399617',
'5de011f2c69e0e3301089e6000a9c852f3bbf7ad15ee8f4ea15ac2cc90196fdd',
'abfa64d226838c80e52b8f0af38855b2c6ae676e6d101395b273cd9107ddbc07',
'da174c7817d37651a29ed8c2728c0b14f552d0f3951c5429aab95b342bd7d85f',
'2edc7c6c8420bea8122c80c07515ac904ecda26ea546923d1acb5442f5ab6099',
'abfa64d226838c80e52b8f0af38855b2c6ae676e6d101395b273cd9107ddbc07',
'2de53e477d363e649022b549a435f93f4b8be4b3e4b00dd2d217c78240c17346',
'3c73e8a5448e1eaefa3a7b0262228ef70fa21e7f0910fe8937c5fb392040d475',
'70632e1941795474cb154415afb79c09b58189afd2243bf81153d59710eb6092',
'89d7a2b6923d2a1c180b558d9644aaf09898318dc40f8a856ca0818f23989d49',
'06827d0df43138142bb26bc549481d44058ba2c1f03ce7702746dbf008449906',
'7c191bbc6a60b37b5dd449b74bd308b2fd7b5a10369be31285f25c216be0b370',
'cbab7074a03bf89c7dd1623e42e65409e3918662af6c65fe2e38c92ff9f0bd61',
'11cc0d1ef2227747d4942b12ea884812700b699a95702444fe0b44c7f631cbc9',
'5181e6b67172ca77d94344a2dbd562f37434378c1bfa7b6db5754c30217b3817',
'ddd93773b42debbb67e99b60eae47693f06f46c6e4ee57403f8213cf556ac139',
'5181e6b67172ca77d94344a2dbd562f37434378c1bfa7b6db5754c30217b3817',
'340ab7a932ed2263ac42cb8f814447eb50a0261880b3dc9c0fd0a3edd6ee0cf9',
'73d3e66b924cb9c19c151573840992addc3fd5766ccd7ccdcb7eeaa6474d9774',
'6387482bc4cbd27eab5eeb91f7f84eb3a17582c8291d331feb32bb089594b609',
'3aae23bfcb6da5016ded9d582612c3ada93f84501c1a294b56f219e93343e7bd',
'dfab8c8714f4b476bc8b1387cebbc3b76064343c61eed30978a1a04d0d455d72',
'11cc0d1ef2227747d4942b12ea884812700b699a95702444fe0b44c7f631cbc9',
'0a7c232a5c4dd0d472d34ca6e768529dffd4683e1968a236a5c789d86837a856',
'c27adf34f8e3c9056006376c7c8cad0fb8286a20389a1d6f0ba40b0901c103a5',
'ccd8d401cad2c824f55cd0e23fee10895f89f46ed3904c7bdae38912c5cd0a2d',
'3f6b10200e9074af4a1d0cfd62fe615e443a5197b7fbddc48b60221fa2f0c3de',
'4b763589c4ca9dc2cda28361fb52535d193ec08cfba6ae8ea161f41059b06ce4',
'540668b4c1f0f0cd6383d83e6d7208181bc0aeeebf94c5a96dc4dbf677321b6d',
'245c04618dc4b8e1048e845a4f370c725fad60d0762f936fa656b13ee6e7ab4e',
'4f96ec322f00df2147d00d2cc8806c3538ae7817cd6a9af309bc10e8da044587',
'2b2c9536aba784e2ed8f4cb08f310d1f3c0a647b0cd0eed201e97ac87b8c0910',
'ef632e445eedc20083f1511a405985c8d837850dd54fd76b30c69d0a801a357a',
'1112cad6ffadb22c4d505e9b9f53322052e05a834822cf9368dc754cabbc7ba9',
'94e5ca456121ffafd66e69e2d5a91d33861eeb3776babe3490e8c2757b5a333a',
'827fc844ce9181342343c4af91e0c3090785ae9a9a1409b39c953c50c1f096d7',
'0df653889283433c663de74c475063dd79df8220bd00f1bf80d29dc5037080e3',
'2c28b486d650b89e08e9202fc347b5bc4549492d1d9b9a0102340dfe6c0d5803',
'510c2a05dab1bae6d5d0b8d82b0da2207de52f3454c4dcf2586ca1234d18073f',
'761354923b5b82a118906b8dd918ad36bbb5d4552659f9ebba8d1b61b4113f1f',
'0eded5a79413150612c13cd087abb09a461913975db8136057baf12346606794',
'083b604917c47cc65c43341c4c2a31a03a633be4df6ec7173c97566c7a6f1947',
'd4c00e3ef6b9ea48276945708c0fc3de967fcc0b4706d36a5233bf68fa6379c1',
'840ee8f7754d93c99da26153f37c0c7576aa04f0092f73400e5f65b69c41696b',
'840ee8f7754d93c99da26153f37c0c7576aa04f0092f73400e5f65b69c41696b',
'26bfccc08f5bdcbc0e4d7f2eda013531859d1224d0adc5a8bb0c72379b2104ca',
'b9452608da8ca67d5c1b0a4bf068977686fa0e6ce5c6a1c4af85ec04c21cd4d1',
'98af38d531fe95b764a461c21ac4429e68134005b59d6d54c29012ed40608e89',
'45dd5f8b635dae077787d3b017e58186d69a23897b88318011aaf44dab41de73',
'b77dcad7687aba3665805882f68d09f52f7345a35a97ebd0376725adef04b9d8',
'4539c312f37283d795affc0622d32f1ce8feac3c87e2651b5da974c0d0c069b1',
'7d15b17672b31473a13fda4d239e5dce3fbf5ea38bea72a35401df0dab869cac',
'2a8edb53e6ed0b16e73b6d5f493938d6c85a986bb3da128438c54ea68294435a',
'4795e88063bdbef4c72196640bd9885735a0ad09cfe101b4a0b35eae2fba4c2e',
'05de5f2d3b8d98f9b929695080ae6aa51b14f00dc122eef1d40d502a0ab48154',
'55aa781d71003229591ba06a36acb5258c7e4ebb8d9ddbeed35e301cdf9ef940',
'e91831fc244776a1f08657eebb3112c522e9362c6320f24240bc42eb8c9d973c',
'a2e99e3d68cf16b10f79060a9fa1f4da93e45f12de28dcbe7d14905668e3c479',
'd95fd7765de11a05d5ebae45406a46c4b3d8f07e39a8e8ecba96ba13ddede5a0',
'bbc9c363d24fb3785c716fcba4b06b8361e78b66a9b99f2a54456299954ef532',
'143571fbf7b7007b76e46c2aa61c4a15994f94ecd75e2fb01e2de8ac7669ef48',
'34869bbe170ee71afe9fe24fc6885cee29b8de8e3e9f506ff9e69a644e8d1aec',
'472e1374c498c09deddd4a172c9cb567cfe86e371b3a0925a25efb9b31764b6f',
'e9c3448952ee9b09ac0c9a13e32b32f20358814e3e4f4fa666064d00361f0eda',
'8b347916be2cb3ab9687c9eb78a8d05224c045bce5b416bdd50169965eb0f45c',
'6a5be3050208359fceb85800c4f715be37e9b3c45c185bcb92ce875d9efc5acd',
'422a682cb2eb3a8e793badce98e8726162d3365784766953f9d1cd01f1eac907',
'9e9c58006890e3e31e306e20518f236965f1d8ee97640175d74afb88769a7c4d',
'eca2315ef50d668305287c1eb85578419f9eb0cedb7639ed933e57876cc9c50e',
'83d52b4363d2d1bc5a098de7be67c120bfb7c0cee8efefd8eb6e42372af24689',
'f4d987e201b3e7489deadd05f0392576412c28aff2fe0d0d4ae70560a22557b9',
'77b65b3949ebd85d177964f81ee8b52f2419c57ce168b6076371f6601c954ce6',
'b7ada453a651edb44d2dee202e916d0adbc441aa876032fa01949f5609d8bbe5',
'1f3e1f27934f401b7b13e7299dd9ff43c415e1769bfa66eaeaf9623e5838f76f',
'f4d31ce155ffb1accba4e3294b092087aa9bc685c68dbfc1bb296204a3cce476',
'06830f6cb5925bd82cca59bda848f0056666dff046c5382963a997a234da40c5',
'e8fc0a4209a6c4a41a05cdf48fa7bf707e6dc99c5f0ff2397b5dac5c2019692c',
'b35b0c462813a700408843ceee40e01ba1fe418a916b9409ffc8e39d8e7d83fc',
'1117fefd09916e44aed7200cde66649b487d898358b3fe59538e55e0cecf1143',
'239e2bd31f3730527464410081ccc3fe67fe6ba6055492eb6272c52d2e164105',
'91f2eecfa102f2b013fa754a33207767c1d444158b21b6fcd5c0e151176dfc49',
'0ed3e5a07372d665de3a8b6eb60c99de46c19622ecb795c19d8fe5b545996345',
'7a4c611c32a2e29a886e24ced771f82e985b4f4ad236cc208537fd6d194efef8',
'b84c243c35b8f6960363e43b5fd8867f4948d94875a43a3c195632d94596e26f',
'7c191bbc6a60b37b5dd449b74bd308b2fd7b5a10369be31285f25c216be0b370',
'ac9a2319ef6adcf69e3d553016e8049c96c52cf159d50aaf31d4aa73bfe01a06',
'ac9a2319ef6adcf69e3d553016e8049c96c52cf159d50aaf31d4aa73bfe01a06',
'0da73d504161705c5398c7715bcab4c2a2a3446a6aeb1effdb0d5c600f1980bd',
'4f06232677c8d6fd4484850237963614aa9ff3027446f5f31c4c18119d897bd2',
'04a1115163a3cb4e69bd8bc9f5c1d124692160f7c955dad7e99dec399bf43ca6',
'f8237697091a981a33e6bc9f262e463a624bd302cc22c89858965af613863248',
'b5625ba22fcc912c6d36e9014e87f1bb23245048ba6c143562ae71f16122a9a3',
'd26064b09f833fb615228dbc34762e37e8f0ff67079e7722ce68e94a7f02fe28',
'4d01ae003b5e362c46002e7f413838c8e3f5fd19a21658e0f85d4a189f11a024',
'4e58dead6154dba2e2bec33ed907d1bb623900935965ee891090dbf5ea5bb98a',
'936fb334d9bf6ba6c5d21cfd751348e61d80d551023f7454e3beb5408c21e747',
'32fe137d1af5b24ffa55bbeeec5eef09cfc6e93d2b291f7259bd8916e85d39e9',
'5fbc69d81c94c0fee87bf0a93b0d8d6d0d9764dede4f2e5481d528d07fefb1f7',
'6d088b653a1bffe728b9b17e5c7afcfc18d85f70502feac83400524eb6a8d5e9',
'8eea02e8912085962a930b28beed2683a988614de9a339750ae0b3061e2c6db1',
'2b14efa5b01b30dbcbecb2b8353904c45fcfafda4fee4177abcba93ac55dd76f',
'd5b631921b2d11b58040be59c3044ce86fea6d0b3bd2f697f5c78aed48eb940d',
'2a015e98f9548a6729f40791a9aaf39cf2ab9ac582dc5aeac504aa500e2e84a8',
'ea145d877f02368b63cba03570dee07b1e880e7d58c38632542d77f5f294030a',
'e9a83570a09c295e747b345333ddd1c7686faf71c645e01ea1be39f90cad80a5',
'84c5544f53115fc94a483e41acefb74a2ab2e0328980a9dedbf8d18b5d9dfb95',
'000000008038ab25383cf3771bb5dc3ae0886c7855c0cbb7896a6f2268ca22ef',
'668125e3c50736566d639a010e065fb570e454b728ad81e025c1fab4b5e50862',
'615472a531cd415a25a0ebd13e2b5dff856d911c2b39f0ec02c9479a2da1dd27',
'6d8097e0408b204f236de86f311ff89e9fdfde32739f16e07f8b7ddcdf70a7c8',
'c353b1bac283e1fa2c3d7a9b19539cb5c4c0ac9fa95b4f60c41e0af6f3b5d9d7',
'ac1247329368582da5b362f047155b94f6a9f80c65f3a32fc9154bb20d0a8018',
'4b4acb64287eaeedc481aa36d2e38d7ffd4e2a5f8ed5df6f458c473c1a587d49',
'e85ed75286cb77475776c1007df8c4ff1c9c68eff91c3627347b065c5bf4dc78',
'ba697ad806a8cf9bb0d1dc31cfb34d5a1f7b727334290db667c134090c1b9a5d',
'96e3beb0f78ec0bcdfab9c9ca8c90a42f5926dc8a1f05ee39ac616606b833b21',
'615472a531cd415a25a0ebd13e2b5dff856d911c2b39f0ec02c9479a2da1dd27',
'aa4e57f414cd7d26601ae60b41fb5a7288dece1d21955572409b67b85f039a50',
'9e4ff2589ae90230f3d7d2d2d3da20f6058d6afbbcc8016327cf1668fdcbcaf2',
'1a48f9a5557844338cec3b482d5febf71bdb5d52a24a223cc845bc3508ff8c65',
'e8d42364b2febc6d4d28923aa84efd592778040a05c28dc316cbf03d9ac4edff',
'6a831c3f8a851dc9a28f30fc6885a129ca22517491f8c5a247b5aaa05112cd68',
'9f3c476980f28ab06c1ee2fab1634878e705b288d33b3c974c16e0273b719dab',
'3c5684d95c5097e5880e4c18dd3bb16e9a5157d5644feab99c63c91f9c54aa7b',
'64473dff703a9c83664674fc31ead21f091591ffabcf5699420c5733b7e6628f',
'8a8e29483b8b39543b8eea5dfc8b38dad6fb962053bab1f04aab6c673682043d',
'060db90965790e88d9c4c5ce339aae4be1ac5f7512bc95a11f30303ae7dbba69',
'060db90965790e88d9c4c5ce339aae4be1ac5f7512bc95a11f30303ae7dbba69',
'f2d544f414aba7cc40bda743b01d85c985ba70ae31275290b3813d297b992097',
'96330cbfc6e8ae87d0608ed35846974daf71b3c3420ec6ecd57f5909affeae69',
'47aa63c1289c7e6bbc8f8bbbf389595a9072e0160818c3626d6261ee995c51d0',
'cc9b429beb13c60c3e59a3b275c55d9c2a27a0300f283c73b1785cc0d9ba0a8d',
'0ccf37f49394da8a9b5a062ee9210046d7f0e18c7c8b886c11e7fc405e8cdc50',
'2b14efa5b01b30dbcbecb2b8353904c45fcfafda4fee4177abcba93ac55dd76f',
'b7bae08a4f5961a558eeffb151dac209f2a3b94686693ac8f4195b468e2b6faa',
'c9e5f2dbadffa52690b366857382017d0f86252bb0e37bdaf6db4ad3cb67f97b',
'a760646c57c9970334f4ad07f6e008ef011260d25f75d2eea6f3c2557a4cfac2',
'3a239e488938f95e2f9450a7af85d0c6a02df0ca522039c7673f50488040fb92',
'364270cca3b094fac7f3a251bb312d6c07494f62d65034f559f350860f126782',
'a276300aee21111aed0394e2562fccbd3854e364edf6f557ee5a14f3fa0b707e',
'a1e233d114eb79a710024aa5f43b4cb3e1496429058f2cbd22af0ef3a0191453',
'd684e2cfc222e05d0a1aec954056ed58c43a61605878ea2fb88310aaaf7641a2',
'532956edf942de8e977cb5adbb8aa204f72871f79295a031f3550ed5892e2d19',
'7f6398bb472d989cf1f75a5cbe2ab404b4d538940e6d92c2cdd8aafeb962bb18',
'cb928605fd377e4c54cb57a710e966140f538fe798acf8e83e7a6885bd6b899b',
'cb928605fd377e4c54cb57a710e966140f538fe798acf8e83e7a6885bd6b899b',
'ddc6c81c03da216550654f73121985d8f30636aac98903de01993746bab7bdb3',
'6963352e2b70e9a2ddd995e02ee4db289e90d0d9b83daee6153e2c5941ae58a6',
'175ce86632bb3ce3f290ad233a277bb4a96466218e4f92b28cff45decfd5cfd1',
'01511e32ebb9fe41a813d71d8da93b09f083f9f67df8fc5204fe06704858ce01',
'0dc42c9fe619c195a814bb08102b18b2b0732e27c66495902eb11b15472b375d',
'2e8d1aa6d69ffe3f1cdbda0f2a440e829999e02b9bb311e6606f016b3b780b77',
'dc1c8f688dbeec1d11650689e53da08a8b7c4297ddebc367de9e9c4a7a049c87',
'dc1c8f688dbeec1d11650689e53da08a8b7c4297ddebc367de9e9c4a7a049c87',
'653ee07ae5c12d797410ceb4f0f95bc9d7623b752e004e2d541f1d7c3e5f91f3',
'653ee07ae5c12d797410ceb4f0f95bc9d7623b752e004e2d541f1d7c3e5f91f3',
'e0a9674b88d1c81808c4931ef73f86370a05b7aa6938640d0159a9331eda0153',
'b83016b06a8241466ff3dc0fee92c2001b0e331ecf7433925e0f8f208162fc2d',
'17c81daa727ec55965421dcdfdc42467fd1b9d88f78ef3c6cf72bac86998f1ac',
'ef7131a1d6fee430e80864c3f211b01e29076f24a84b0a6dafb223c87355c999',
'bb17befb8ad16555e8ae0b0ec8b11a3e90ecb09a874ec2049204b4d66ce7d827',
'f42545e0c612b065f9f48c2d53d0ec73218b9f795498d9aefd04d507a8c02e9d',
'cd8152743ace92cb4969010052ab7bb1d87f03b7c1b98030fa5a14f8072107d4',
'5b5816d357e09a85a61dd1aea0b2a3d2b85dcf69a2c1e426031e4753cfe622f2',
'4848182c5d654c40091d9fb4b8997b176553b7cc3d10cc12d0471c42ca209275',
'e6e3e3b5f0ecd459202caeabe24f61cd2cd4410bc53c9a4cf16d159be6c355f1',
'dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319',
'a9f9a6245b8e03d99dd26d3bb5611c5120dc50a5ce0bae52e90ea22f7e2aa5d2',
'facecc1b8dea6372722b7b45e65969940a28159124ce60fc1f8d7d0c42f6f0b4',
'49b82e30df2b8c7009347ce6edf34e48915e4f1af347f7aca3830b1550d395f4',
'de54372665f0280f0ea353199757d517f17d4b4748fa482fa938d0888f3f64d7',
'96fc0da7dfef1b80ca3a5fe8f339d4cac234b6dc7e50cfefe3a9b8a1b90ef7b5',
'96fc0da7dfef1b80ca3a5fe8f339d4cac234b6dc7e50cfefe3a9b8a1b90ef7b5',
'975091e8f7f96a80f6044a988e86e758f519926c08fb99d767ab8376fe2ebc47',
'641fd340a76239260fb2a6f41c33b4e697be4670221fb28f1598896bc777c4fe',
'caf9a962662ff1fbed5312c9cb960a4c05d0e68e00fd6e5cb6816cadee87892b',
'45008abfc87b0985696c5821c7ed87616f24584cdb565d7db43c33ec46668c3d',
'ebc5d7795a78df537061d7fa276284a9b881e692fb727022dbe7b3f10f5e24f0',
'5f1623d9b62000b1b0d49dd3b39025aee932784b44e20ab52da2a28298ce9871',
'111647ac263ecb407f191dea6c5dc77ec22c99274da8a10a2c694d6965d03421',
'111e0bd584ae87ef05b355695435d0abcd761858d15bb9f31424b36b75cf22d1',
'41146d8abb8f08902af3e1f4b6dc2dffd3baddaf94d01a7daed877f61ac47098',
'efe242cbc638659f67c47d52c6a1b0722b9751d178718aa9cb3633de75704ac9',
'd1f45b853cd86f483063e06949eca575cdbf4ea5cc14fc5bafa972df214a1fe4',
'12ec2e70f8fd069cf6efb2b443309f662f6414c2dbbdfa4999b5b0556a3094c7',
'5fbfe0f5b54831848e2edc9666c38b9fcdd7a37a2a36f09de82578f1c70ba7cc',
'ac4b66f83b6d4ede8ada9abf24379292ee46055a34bb5f9a99979c01d3da4c76',
'9235ed91a08e09c8c901b447891ab45b62696f1c10beca02050bf0f41388d2ec',
'1a5d026f137f1afc188a7e9230044f4446510aab01f0143898fc484bb34f9e8d',
'df039e2aa97cdbd9fec2d84e238fdfa8ce38f502537edfddfa097a32a08909aa',
'031bb1628e4077ae030a48568a4d4176cb7736c04c23d8f65dbe5c0c57d8ed4a',
'efeb647cbfd59f7e9fb81f5c5677b3e77cfa85ba15c9a9528392823eeb0a4bf8',
'776bb96e5f8408a548c38ea54bcd4f8b686eebe739e43e6e40183f3fe42be650',
'a6e533020c1cae4ddbc1ca0c07bd06e76bfea646e9cf94ca105f0dedcee6a911',
'f720ed6d341e6e92d61ffd92c17bb884f401dfa4dd68d0f8c4b0cb7bd61740f7',
'e8e1ec0aa8819c6b43a17ea1307127db1991ebb4eaabb1f104b95df1d20dc012',
'df7944ed0d2cae83ca2737ce8f6f23d12c08037297ebd6434c45e1237a2a8e7e',
'4e7dd5fc6303dcf3cfce3e1566830286b9d42bff17960a97191a856ca9f0df05',
'012f3e11fdc1546a97c2791662d1ae6ab1a0d3ec3fe3ae31e861e59cfde1e3ed',
'9dd13c81d2c764d9df95642c59c1a3c2ece8ade5c4e186afb7866720bc44d554',
'1bca977c8d1dc9d585f29e3b16891ada7db1700cbe4b46ecbe1fe95830df323e',
'6015830d41341ca4943cb254dd20a7a278793c9a6bbb66c96415dbd02c7c12d8',
'f9cb856e934784ec9a4b6eacc394ad8743c1d06d8a30bcb667dcb0b2be5d7a4f',
'89ebfd493f6edc7bf454596b1ac06b2921ebc85ad2d83409fe7eaa3ff21e0b7b',
'4d163989623ea4be9da53327ee6d43aafd41c1ff28ee32c77590daa75e03d22a',
'611b26cfdbf6d37428f2f73bc43c276603c54f6c5980b4f773957dbbb8b0314c',
'686322398a192b725e47639b68e27075b5be24c62bfc7f1d47015063d270cbb5',
'774c374beb0e658eaf40adb2ee17aee7348d9d5b1460505719037d9914a423f2',
'66bf50e9d2c8735930ac44f69a28e70e8e403ca3cc0b441a9e1fbf0dd023c59c',
'47fb96c14b8d680b5511232a25a2ef9e16ab9011d3a1afd742b3fc18c366071e',
'1885ff6e84490c4403734baf466f6e4c78866a1ecdbf9e6546d313b90f3e6437',
'a2e4ef73b7bd02408de36ee4b0c45fc35042edc91d95b6e5192a3b5057644e92',
'5ea2a377d3daabef39200f4f71146df2cddcdb9e9992499744ff41a4d376287a',
'5d4b6c8d4921146c509e00e0930c3a41f2c81bfc06fa0794cbf7397b8a1c1ee3',
'6d6aa6da97b2ac867046e5346562f7d940141c42d5577d2d9a487dccb7424c53',
'b3c430bfe65f19f048764b9cf86176f69cd7fddf5bae28fb1ca4c4d82ad4306e',
'88fcdbab9ee05799477ebac58bf61a3db45a33d00b3e6b7083bf4e6c582b21d4',
'a7997565180c6b3d74e56bd24adc6b41895377ee0f14e46d6c6f5150817fbf45',
'52d119f46298a8f7b08183b96d4e7ab54d6df0853303ad4a3c3941020f286129',
'7da8a13f0066477d4ad811a834c77dbfa330170a9b3f906d123859e83c7cfae9',
'541f028646ceab959f292341aa686e9381e9fbfb73e70efa456d702801d84167',
'8e4d1061f230a792bcd2e55aee59e452e2e6724c649b407b784860377f8ab08c',
'96655b5a483e678e6036cf03fb069d1e683706a4669f31ec84b45ee3e7b03bd0',
'758841eb409f681a181004cc0bf7fd403d5ae1aa40a7cd932959b31bb75a779e',
'6c950fb97e12c284aa492abda76eb4d4d2e640dc98868f3266982e2a5def8036',
'6c950fb97e12c284aa492abda76eb4d4d2e640dc98868f3266982e2a5def8036',
'30aed6784c19bb0f97e9ac5f6f2f967d6024291a5f561afb9b266a5194dcaa9b',
'8f10841c2aa4a783ac0d312225c66b6c39679d5050796720478a74be184ada0a',
'9d04e47a1b34ca3e136c2443f8c18a04b9d8d21224df6dab958390420ecfa12c',
'5208cfc7618f05a148eb60440cb919dd2b81f56df3c366dbf4c9b63765c19386',
'eeeb824753511299431b36e840ae20df7fd11efbc16c464b0624959e5b5afc79',
'7c84d13cc291ad823cd35533d60625ee6c0bf8f3951f91533ed96b61d13a1a9f',
'a0ab832505756616704305f672b1d6023fe381e2efa52138acd5c4f9a465775d',
'0e5dd27d8867f06be49c058f1d8913486ca82d27e262d4608691d2fdaa6c3961',
'208e7bbbb36d0e0546e218a2c8ed47cfb1ad0991d4ccfe7124e889a0ab683ea6',
'9ae5c2c76bc3732fb12ce361f1cdc4fb468a1b9ad9c54092a4b3110079a574b9',
'613f8e9db76b760617c4fb659bf121f006ba4d5936bc15c22f5ebf67a022973f',
'86e9d265b2f6e9a0081e2533781a0e0d2f13bf8c378e2cdb99bf5c8910f437fe',
'7db829c1be315de0286dc59b0ad69fc6bcadcb2ccb157ed281ad505046a2bf9f',
'86e9d265b2f6e9a0081e2533781a0e0d2f13bf8c378e2cdb99bf5c8910f437fe',
'7db829c1be315de0286dc59b0ad69fc6bcadcb2ccb157ed281ad505046a2bf9f',
'1ed2222165485251b89ac078a844e91f71a0c99b7f62177691023f075b836787',
'e41236999395f1795ed7b4aad3efdda6cd42e908d0b6eaadcdffea80c7392b6c',
'86e9d265b2f6e9a0081e2533781a0e0d2f13bf8c378e2cdb99bf5c8910f437fe',
'd8fa27c222d7b160aa60ea94ebe0f86e341222684bb9b0a835634585efadcb77',
'd8fa27c222d7b160aa60ea94ebe0f86e341222684bb9b0a835634585efadcb77',
'd0a56fcdb8e8f5491265d657efd226f375e64a3fd217b34e7e939c49a0868662',
'8f23ea646179916fc625cf7fb8f5d36bc2eb3b7c5a2279b64280c6afd18c039f',
'8f48017e9e20be2fe49747cc73864d5ba883f3a37638527d3bfba91bc9454d93',
'ce91c670d27b58822f09e44dc5fa15be0ca6d823733a9ee07d5e0d626bd23103',
'ce91c670d27b58822f09e44dc5fa15be0ca6d823733a9ee07d5e0d626bd23103',
'cbb0d5db1d63504a1ec617d3b5e0f5ccad2ecddceee6e0071f1731d541735105',
'f9cb6495acd02148985c3f2ab8dafb656835650344422f2191c8874142d1357b',
'5472613a4fbf242ae138020ccc0e59e1bf3c3caab2cbd4a95a0458a238b1657c',
'f9cb6495acd02148985c3f2ab8dafb656835650344422f2191c8874142d1357b',
'5472613a4fbf242ae138020ccc0e59e1bf3c3caab2cbd4a95a0458a238b1657c',
'5472613a4fbf242ae138020ccc0e59e1bf3c3caab2cbd4a95a0458a238b1657c',
'8b8b41a605f70e16790473bd6064cc0fdd9bc1faf4903d8ecb0ed6b279718819',
'86e9d265b2f6e9a0081e2533781a0e0d2f13bf8c378e2cdb99bf5c8910f437fe',
'1d77686d79b84d3651e73e19596b97d68463c816df42db64595ce0cd12011d7e',
'1d77686d79b84d3651e73e19596b97d68463c816df42db64595ce0cd12011d7e',
'2a731bf3485c1ebd2c70a316ece4b2c3035e0622ecd7c610c08c9bb417b571a5',
'8dcd384768adc47dd9a0d4c6eb5f0e8419af9cd2d83dc336c3ce254f297e9796',
'63ac666683ad27a3e34d42c57683a01fab97645c02950126fe524d0134642d65',
'8c76fe349a453db2af8dc02e3f2d40d9a04f4a231fc0d49e43dea323cd0d1286',
'e08ad7768cda30af4ff4140e00a31949c0eeb0fcdd03c8629f92b5065d337fea',
'1ed2222165485251b89ac078a844e91f71a0c99b7f62177691023f075b836787',
'9c026d65c9b747f4ba59e84819da506349e06ec7bc6d7e4461b1093d8b5a44c1',
'781cbad1d3d6ed73444421e765f4801b7f298b52d74dbc926f43abab28eda6dc',
'a618c4d73bd5a73417fbb7e6a5d1818ab63808b317c4445ed34e2a05a9277320',
'ce91c670d27b58822f09e44dc5fa15be0ca6d823733a9ee07d5e0d626bd23103',
'5f98f0eb39f92b115bcbaf6ae013a04483f27bbecdc35fc007b132dcff420426',
'83f797860cea654026c779befdfea489aa9a53cada10d6087076fc5703bd4d2f',
'8c76fe349a453db2af8dc02e3f2d40d9a04f4a231fc0d49e43dea323cd0d1286',
'dd3580373a495e9ea4425feb420df5714b2e6fc08af48be1a62af804d3db97aa',
'62c4b2e9938d9a06d9de8c3d037e20eaa576bf4ec276134cb19dd640d890993e',
'3f7dad7863dca84b79571b4d439b3743e65b4099c3440f4727064093d0fa6fa2',
'84e6abe4cdf74b3826e3f64b181b38e40027dcbf6d69bacb8817ac3d9fa9da37',
'58ddc5369980a4d4c3c2740b20d92f95619d3703206acd7b280362aaaceec11e',
'5df824243c6045c222331732b07785138f78614da3b6bf28bfb157a87297333a',
'58ddc5369980a4d4c3c2740b20d92f95619d3703206acd7b280362aaaceec11e',
'58629d59e12d235fae3e0b34420d44411717b550de11b25d331424f2f6f064c9',
'8c50ce5cd1ad945249848b785ac89c1e8aad6944f48d431eb1c99130f983119f',
'aec2d8d405ab42914b0f4795a7a993d872b4cf216736cd04fad889bb05acb8b7',
'dadd7380bcb29e526c7d5772816c5105f1480bd8d53bd627ac1c5d45cbaab578',
'46d44a473e01cc34e32cc60d8d3162dd7607351e7a3479fec7015d2271537a81',
'46d44a473e01cc34e32cc60d8d3162dd7607351e7a3479fec7015d2271537a81',
'46d44a473e01cc34e32cc60d8d3162dd7607351e7a3479fec7015d2271537a81',
'094bc60d4430d900638b6fce7004891af350962c6c956641b2bf03ec5543f305',
'8c50ce5cd1ad945249848b785ac89c1e8aad6944f48d431eb1c99130f983119f',
'ca534ed4fdfc4d2faea5be173dd08c0b208328fe31f9a7f39fc8fc2e1efe43aa',
'9298ab3f9e7015b852190c4da8c22229fb55ad42b35741617b1415a05ee54d22',
'ca534ed4fdfc4d2faea5be173dd08c0b208328fe31f9a7f39fc8fc2e1efe43aa',
'cae8c037b58d633529082d8a807cb5b9de8be7e7c8470e42e01e9a65ffe02e7b',
'78733951a0435da2644aa5dbe6230cc0624844132a6fe213e59170bcc7dd3870',
'56bc0562257fb35ca856073b99bcafee346e0e15eb28eccbbfd9a66a7d8bad22',
'ca534ed4fdfc4d2faea5be173dd08c0b208328fe31f9a7f39fc8fc2e1efe43aa',
'f65cb38953e9fe53c2c11561c8751335782d7f393b9c6b3f1452107e519583ab',
'23a1d40c436719d5b308a367ebfff0826c240a37658939b2238a64f0ab8f4cbd',
'e9d79ac73746ff4a501c5c520d85dc3e4e5951b978cee266b0bb95852748c812',
'd5d66ebac4a21c9580d9201ed476168f9c1427969318677a3367ba5ed43e7acf',
'd040ac759e542f1490f435856d2ac94e6f3dbf622a52021537b671e8f4e440d1',
'004b46dce3cadc9ec2b930cf140c921528dd98e73a27c510254f567546eb224b',
'58ddc5369980a4d4c3c2740b20d92f95619d3703206acd7b280362aaaceec11e',
'2a731bf3485c1ebd2c70a316ece4b2c3035e0622ecd7c610c08c9bb417b571a5',
'58ddc5369980a4d4c3c2740b20d92f95619d3703206acd7b280362aaaceec11e',
'43faba8b2cc565cb757af10a19784bd9fddb5badc2a881edb0baadb27b1fab46',
'9d3404bbded41fe22a13bc18395813c4392cd9080e3812fe1a402b76c159ded6',
'dee800dbd37b18cdef4433d5182f3b434ce4b9c1ef92f496b082ae24d8e7e123',
'49d7aeb097b5cdd3791ebd07d8ed9fe97e24d9d8d0299bb0380e4f5fd0bcb937',
'8c50ce5cd1ad945249848b785ac89c1e8aad6944f48d431eb1c99130f983119f',
'ada1944933663a158fb39b6928d40e7dd02d36b3a3210e63a5d302465e0e0d93',
'58629d59e12d235fae3e0b34420d44411717b550de11b25d331424f2f6f064c9',
'f42b5f7df9f15d1862f647972927a5e1010b9940147605378a4ad97621456187',
'ada1944933663a158fb39b6928d40e7dd02d36b3a3210e63a5d302465e0e0d93',
'ada1944933663a158fb39b6928d40e7dd02d36b3a3210e63a5d302465e0e0d93',
'52967f924aa2519bba5db07fa2ab90baa5904d08e7be24754501f175a5079bbe',
'dadd7380bcb29e526c7d5772816c5105f1480bd8d53bd627ac1c5d45cbaab578',
'dadd7380bcb29e526c7d5772816c5105f1480bd8d53bd627ac1c5d45cbaab578',
'732becbef286330ad874719202ca1b82691acf6ac7fe26d0347b91bd8dab8678',
'2a731bf3485c1ebd2c70a316ece4b2c3035e0622ecd7c610c08c9bb417b571a5',
'dadd7380bcb29e526c7d5772816c5105f1480bd8d53bd627ac1c5d45cbaab578',
'dadd7380bcb29e526c7d5772816c5105f1480bd8d53bd627ac1c5d45cbaab578',
'dadd7380bcb29e526c7d5772816c5105f1480bd8d53bd627ac1c5d45cbaab578',
'82abf9e0b3d1b689b594f74c8076434a7def6e9e4bb125647279f5d525c56464',
'f65cb38953e9fe53c2c11561c8751335782d7f393b9c6b3f1452107e519583ab',
'732becbef286330ad874719202ca1b82691acf6ac7fe26d0347b91bd8dab8678',
'7fa391242de5d76530107bc1345216d6f1b933b25a97aa5f1f4c6857f0dc488a',
'1117d5202bfabdd4425b700049efe7a40a948e961b3004a3777185bd151f7131',
'83476c82d414aa7dc0cad4a6c29b59e45c5efaa7acbe4437f22a2e5eb701bfc4',
'687599f20849e801cd3b9f7734aa66dab775c5208f4851ec334c4c87ea02f8d8',
'f64e08df9d1fa4eb9fb95625fdfdb003086766030a7b326fd4e5fc7e31aaba95',
'5b3c33430c4e726c59210cba723bcf982c8d0bcc1f7d80f68d50235324eec061',
'5b3c33430c4e726c59210cba723bcf982c8d0bcc1f7d80f68d50235324eec061',
'2cf131819497c71793381ec4a01d96e891fbe919d98e086f0df1ef7a05b05cc9',
'1d77686d79b84d3651e73e19596b97d68463c816df42db64595ce0cd12011d7e',
'68ac0f27c0545377ec6e7c5ce6aa2d6ef8aa1edadc6a8c2ffae8eda07f26affc',
'49a932ec8ea3a5b3b423d91ea695d34c54e035e69b5534bef1c2c78ad5678cc9',
'6d7ebcad7e5d83a9a8d0eff8995b2b37959bc34564967fe7962e6427c5ad5f33',
'592edd9e91aa2fa82a0b928e2db701008abdc8aba9dd626ef8c1c8de0ebc457b',
'1d77686d79b84d3651e73e19596b97d68463c816df42db64595ce0cd12011d7e',
'0811b6cfdd9c06abb671a1c96c3fe7e4d20733dd2d6e0445c473f9c2d0975ae5',
'57d1e2d2fe77cdb35a566d11d0123107f792b8ee8d0b007723f9f38676772e9c',
'111d981b006ddd9ef06972261a4de0b03a5e6481657155d8186083e9b248ba17',
'd8b158d781492916b9bb55be6cdaccbf90ed131c6a8b0cc4c191d133613f1323',
'19c6b9863cd9d7453cc7cf8dd6995c81e5e76c7704e6707094d47b57d95da62a',
'9be001d7f24c518926bd73fc06ffeea273e270a1c43a94f0d89e4887796a70ad',
'9be001d7f24c518926bd73fc06ffeea273e270a1c43a94f0d89e4887796a70ad',
'19d839b307dc2bef21f04fbe49c7801ceb6b18ff1066f17dcc4ff1cf2bf038e4',
'fe2ef1c33dd4f8969c9f72cb74e96878c2c2f123dcef9d6566ce7415bfa34203',
'041cd7cbb67898d6d71056b07e34afc462d927fc769b8b19177e438cb28ec71b',
'041cd7cbb67898d6d71056b07e34afc462d927fc769b8b19177e438cb28ec71b',
'48cb87a0c3859702a95a8d0a93ce14b0cbc299492bae14ace2e62c7e73dae135',
'161ba32df8b562b328ef5dbeea88e02d8a2dafd53f665bce95facf9d0ef49bea',
'1e53bb60197f6b5bd034d74e83f5a809a0bcbe8d812666b23a35e8d3bc8c061d',
'1f940fafec37fdffe56d39358ead86456ff45c7fb1820dcb8da79c8349d12fdd',
'594013f26562ca364d00078970ba82406184a6e7f6886bfc7eacaa199ec454c9',
'17c86799d57f6037d80dd56a957d05699a7d4989fdf7c7b3d3a109acb21f65a1',
'b30d2ec9eb80175c9389e5dc73e234b80da1b35967e511ea5c96c46ab0c38ac9',
'b30d2ec9eb80175c9389e5dc73e234b80da1b35967e511ea5c96c46ab0c38ac9',
'59a9c4e02bf27198396affb99ac97d58dd2ef216e66feb37437bdba9c0aff26f',
'4af0436570f40502a8a86789df9be3fa9bc48f6618a6ef18ba29d85cbc96f7dc',
'b8056453ba289ced9b8d279865f5f14e6a9f152a4c15550a05da074018bb9558',
'649835abef50b3545870e9b9cd42d115a85a9fa090f7ea1d4b437ab6fbb1eb95',
'329faa346370c5afc355437f01f74bfdbc8672850c40fa46cd34a743e38517e5',
'329faa346370c5afc355437f01f74bfdbc8672850c40fa46cd34a743e38517e5',
'17d81ca77b3c6623bd138e70e99fc818814ac4bb3b00b7c6e4c43ef9b08a2b5c',
'0fd7462f337cba7ff1b5163db4fc006f45afc903814021fae0aa483aa996b879',
'1d45e9d62dc058257daa0ee9b251d957b823092852df84b97343d659b5972989',
'249d96180ca5d822a2388c2760f2aa24be8291873914bf92a511204087cb1793',
'2be580e0b9f369492b474c6ce0fac29ee3873bdc4016531af24a16be96ccf5e9',
'0421ea20841d04f873a11933dbca4034f3fc0b019b8fb01c73e85b04f65134e8',
'0421ea20841d04f873a11933dbca4034f3fc0b019b8fb01c73e85b04f65134e8',
'fda49149c0f2ccf78395b078c7471af49d6182a1ba883de82f9289261103408b',
'5cdc68e0d54ac7d1c914d24ae52a9707030a2966dc9ffa3dde2cc4fddf34fce2',
'7203f6027f4ca66a92b201f911d7d165add10a008e1282cfcf49c0b01fe2b0df',
'e19c4d9c607c3efd442a1c64f739824b3bdd3485cd8063e547b19f2352737fa1',
'bc03ceb67bcc3104f9a13694f4389b6b458e1531d360bdb6363afd30e764969e',
'd0df82f3f78d2d67920c39b874b11b1440d0bc8f950bd7e1d08e7fc519efdb01',
'ee9c46e5cc4022d8bd8cea507b03fee1ac0ac7ff8927eadee67254f170e91877',
'03ebce443f3c981a014f527432145660a883755ced03ae2f3f8fa5f33b04750c',
'5cf06ec3fd29af320c97c477e64b314661f7e17bae92010091b5da0382c84f9a',
'978bbde406bacc9ee952a69927a747281fd664a31f6f8cf7c2769d0b4124ec17',
'a9f627e40c438b8d84c507e1b62218b64f0a63cd1cd1580abdbcdefb15d0fa22',
'fc9fed9c70363b503336e7de188a7aa204851c05169e366aa83b96d649ffc399',
'a9f627e40c438b8d84c507e1b62218b64f0a63cd1cd1580abdbcdefb15d0fa22',
'3b048e596bf2442e67c1886b650a1417f29dcb32b0301385cb75bfea661148bd',
'732d3dd68b716087b7b8bafb2e7601ea16a0647982d34db147cb74ce304c1538',
'f2540c9c980b69a0eb1fc3dbafeac03247a2e8dd0531e6d5d48e1f533771f899',
'a239d30872e7db42c492b718ce50d95149702d90ef35fef4c57086a8b668c34b',
'de0d374704bdd76533edc90b9c941995839e781083718f7314d71739808d4545',
'2c1cc69a66ece6d6daf6f3b1f0f24a367dad32c106fcc3d02ca3e1e80ca6aaf4',
'4c7911392931653c8c546ac8d0a4ccc29bb07eb94c1298d67f2c06560fa7f167',
'c35dea2b35fce01e842d367410428c66c65b8dac04d92bdf45460d62870319bd',
'0e39a11ec291f8c7add5b094fa32a6ac2552275a5ac9ae62fe9d4de89b44a8d4',
'781dc3d8ed6fcc31af7db4089f0d47ce85e3c71893f10719647b655250879c8a',
'bbae658455ec2ad6871d4d23bced5ccd93d297517088ebf3e881abb7d25a17ec',
'7efb42e3cff596dc7ad0535cbd1198f1bccb914c7887bcfb44426945d7b45920',
'e221207c9298c19a158668b69183499481ad73eb52a095bf7f7bf7313dda52cf',
'b83b0f7fed19df06366ac01b2aa0d0ad2fcd8a38108b0eb254ebb1930926ae99',
'111cd02902d3e849b9cf3879efafdfd4587d8b51b42215d9bee8c78160796480',
'111599ae9b7352b8fdd1fe2a6a39b3a73116efeaa29e598f3b65511b61d68dbd',
'd39761c7c48c86e1696c713e94397d2611bfc87858f6ae86c37bcf58d6d7ba5c',
'6a1f184b8a60d221403b8ca18cc76503cff5425b7504ae739ff0e049768995bf',
'0c665d87f8521d6c9a03ddcf32196a6a26ff03dd8cef3c48539934730620e43d',
'fed2e15afbe8f989a3c3e4cbbea219b3ab338dc4645d786683c6a966d07a92a9',
'74de4a2bf511767278ab7db059fa4b4653e55057cad82085e5b5acbaaf978a3a',
'c6e45899161bb3e34877ae13c9e71f78ec52152cccb6157ad89b8e6282f5735f',
'44b475e971de89de3baa21c252dd784bf07f5584cbcf26186b294a83171149f7',
'9af8dc62f126c0360bbe4588e5067aead1aea662542da57f33ca032259cf4234',
'44b475e971de89de3baa21c252dd784bf07f5584cbcf26186b294a83171149f7',
'c1ed0a857752a8613c638777b95bdcde9a17d43229aa00bdf5ed91ce97defd85',
'60aa31f16e38c785647c438db3391846c4d92f53712b6968d9bfc01ca644c061',
'41a9dab48e94bd5dd65bdc5446e0322a68ad2e5a2d7afbb1ca84cade684821b6',
'b9f9a96a5cd16f18890ab45c09733cc427189c79a77ebb8dc0b526a0941d5863',
'736ac37ea560e091b7bd858c82be3f252cf024d94d579cd5bda142134abeb2c1',
'ac00b23884958159f06a5815e99d5a1d18d00cd95012b669efd4c6e705e69d16',
'83b593387827276eceda8bb6f5087ad2a751bf379ae9c56e8abbfa407994e285',
'4a61ed036a76931e831509523a03d19fa03c6e9a35d6f24baffe841c5ac31bb2',
'bc0742bedb545bdd8f72c2a4f8b136adec28dbb7eb42bb5d9907525c533207cb',
'ea7073e92b7040c9c10b6f594fe4db31f7f1ebc8acaffbbe64ed02bbd2ecace6',
'932dfde8344575b4ca6e2a4f0b7cf814147294059c5174d7c1f560a4db2af438',
'998789ffe84afbfe07da209c3cb07099cfec05b7ab841415362b2718922fc9fd',
'1c9767b4600360cd60c84284e189f5e5d1339eaaf69ee13fc17378d49c992191',
'2a805b4c5c1aaf66d320ee2067c8e11918741230a784c74590c75bf68d42dcb3',
'722378d6a28b29b6a5b9cf0455035353cad76e2db532982c239f39e9047987e0',
'51f917a4daeceb0dd707ca1b9ae26b80c2146760e8c09cd81da3faeb87a0b603',
'25b6fba307cb38f58238203366cf24a23f8475dfce24b6147e42a3c6a72826ca',
'67f961351272baa9171e4ca900ba902e7ff515f7088dc5cec9305d118b0343e1',
'bb8bd0865f671bdc2246fdf8031e6e1c0d9d80a25d610deefd1c46c01a5be2e0',
'2234aa00a1fa119ea45f163f85483e6b171b2811f67d4427d005ffc05cf0dbe0',
'e43852245e98acc1bf1824fe71065080ef5bbf790593d714e56bf93769456008',
'63783346456bbf96617ed69afc9383a3d0aa8135caafa2e0057fbfaba56dc0c5',
'48cda3fb9091f77a84d72c2005ed4f4f7d1cae738315c5499679320f2f122aae',
'03a30983e2e28febcb56ce51bab952009f7291c8a51160c3f90de532088be8b4',
'97dbd777ca21cac6d0b0c7bbf9db337364f9a0c5a5055df3b824b23c250bf310',
'eec81e673686db775ce072c93505d15557ff428015b259903c3899c84eb2e09c',
'57e1ded4a1ea2090a789405751fadc9b50040d816c63a89deb0cbd3991d65cb1',
'ba8b6aa1173db3cd5ebdf6737ef0e20b5c5f7ad43f909a1b97ce7eec00d16bdc',
'b94c29e04903913543f08546390f59c5414ae8b63d4dddfeac85f1bc20b6e52f',
'f97b03a1877377e0d74218d06e0b15b92e7b5f7e2fe5f67831624eae5acfd85b',
'478d32f1cd8bebe0d462b94e87d8bddd9589222ff5bd9572e1a557e07836e818',
'0cc8e54982ef2357ff264ca2342d54a4d0f6c75492237ff3a03ba0dd0f85892b',
'b9dc0acd1960e4bdf35310d1c2de2c120c4deac8324b8f996153e8414c4f13c7',
'bff02feebb30b28775a7a4fc136ce0d56626bbb70c050b28959a2716e4be52eb',
'04b5c88c1e0415a707f539dd6336d1604f4a022181b3ff0eb4b7d9e6c76e61ec',
'358b81511c2e8f693b09016d7b92f5a2d00e1c29476daea49b5ea70182ea44a5',
'f521aa3162728f03fcb628e1c60caf47d93679d32985e84eaeee8476e8328182',
'5af2ec30b0b35679da9f4c3ab85604e48494fb8c379dfa00ce28b44c87c20d83',
'8b6e653db3c1c95c4904acb9a5e8e3b73c0332ca002278167b0c725c4fda8771',
'bc051cd18ab114b3dbd58dfb69fefd20f1d2e64446f10af2e07660a730797958',
'893cd86c8c08ec8fdbc5a79a1ca9efb030415e032db9ec4ba241d67de899d3ce',
'5d5098b8f69d2583e7a693c288b3ea0585bb30d463842be0d2826b0cbf404bc3',
'e28b4c4bcd085dc7f448a9454966e8b3db90a03c79806ad992292cc7b371c0fe',
'48cedde3bbc69077fee32cf9fe07da0462c2e87e51bce6071cb39318b93d38d7',
'e81e89fb0fcab106888fcc8508e037e1825feac7415667d9b53cc9b29ee690ce',
'9a5cfd7dc91ae921084b937f8a09b149bb63fa43299681498a08e4fad0124635',
'cae8c761483c884190df49b836c7b5fb29bdb9873902a737b2359162f080c0c0',
'02df1f929b133d179011d679697c9bd9a20c081b85c629405a1bbd9ac775ccec',
'542f8288c4d49c3990541cf320d4d3abfc08ca1e2a56fd41fe3233598109c3c0',
'd2317a89144d0f4cb72ea1b0e6db4f2ff98df14914c9adb27377e5eba7af10e4',
'15113a6181c22771ff957f6c292acb0b3ca3945d85b23fb1e4d01e148f0f16fd',
'9fde66f5003c7bbd854d57a5b4cbe0907b75aae206dc7a0d17beb0ba0bbdd0c5',
'd45d97a1ef993abf0ae397ecbf4103c2b02f0e45d2ed14c2cb16e9985a1407ba',
'09c44bb66e0e8bb3d713d0058391a85452831f34e920c9f4003c9564ea7bacc7',
'995b8818e2ad672e1a0ebe0ba07759ed2b412e8cb6a57eabd789355e9c4c3684',
'148e3912491dc73ad1c74e1adf8b6ae064669d0ad12ed782d89fb9b01b174b4e',
'733c5427f55ceba01a0f6607ab0fd11832bbb27d7db17b570e7eb7b68a081d9a',
'5126069fe7d9f75a47929ad3845df7adb5826cf1d3e607710507be83d3f880d1',
'f49966caaedfac1b92bb149d228c8ceccfc0bf6f4d02d9108480d7914e345f63',
'7370b6dd2a5566e3fe165ea665a6d0638b0d445e71abfaf0d9e766211acef477',
'5c1e5030af12a418651450f479a6e389af3aeadfee9dfbb4ec4ff514139f4f67',
'4e5cb0b4687465d89c8b25046571c1895b909723310c0d2350f7defa2eeb3c50',
'a7046bde7917676b5fc7fe535968809a97a76da8cfccd6b491b74fc81f9a7acc',
'c866b2563151bb02d3a3e482e5aca6f1d4a5af07e7cf6e1cc3a444685b404035',
'608609eb86e1aa0bc0197935b1bafc6ee7938fd41f96f305fc178b7c6355c4a1',
'56734ca2a76c0165bdde1fb9d97cd1521a59f326b20449e4e2bb3fed56c947bd',
'9c5e4b3876f0ff19b45952c5b0daa8cd19c1456c562facd0e42747eaa41b3b5a',
'e13a2cff33c6c3a93fb992b1574e8399a77f60bf333dd19c9454c4503ee8fbd1',
'fb8442831ccd311837cefba5d2f0f9bc381fd8ad480eb6ad2126bd125e83fd62',
'3de109ee01064af1043769ea2a82689dfc934e6aa417fbb213fbdd41229f6958',
'f4630315ccc71fb14ef735d1afa969204769b50f4e9226281b44610b2985fcd4',
'641981dc226296f449799d5d1d2c669409d8ad95cee8f956fe2d5d7946d14287',
'388420486e55835d6b9dddbfb779b05363c1af5b19f42a08c65617af82644fd4',
'fdd9f8bf8eb9f4592a0e8ba50a0e8c645546af2280112898bfd90043db780ab1',
'9ab60a924484d6891179e941cea0a52e4c975f6a69b9a0c7b6bdc19f234d1018',
'5cbf5d5ec04876e24f6ac5a9aa7f8b39e9dd050859864f4e8753a98e491855a4',
'a2b1c0ca2efd8011a5c655b79eeeff70777b4a8de24b6aa9da1f8fe8d7af21d9',
'e5b07e1423363fdd1b460a800abf36a035e209dc912969b61c23cddfef0f460c',
'cad65fe59dcf668b9da88a7bd6cf85789231d00d2e2f0b5756d5c97fc2d9ea4c',
'a47df5433b66c9731a3e4ea78e402f77d1caf56844e6a95784bb1a05b1ea1b0c',
'24c637add93e1f831a5c065cf9dbb6bb2708e3f219f6818513b945edc0ab4ea7',
'77ba4bd8822480b42977c44d8dd8a35201141e602e233612d3fd24685fa303ca',
'npub1hlczlm4mxzegwad85n7pxm8q64nzdwahpszsk2y4ngn3de972t4sq8n0h3',
'fad68b7819813ba23cd66ba6c987c74c4851d871e8b86201aa6582cadf4fc4b4',
'10d154a87dccaf9edfe0937acfecd47456dd456c26607a33b2e2957bd83c878e',
'c33a84b9ce9274f04187aa450638686f02553cdd630b9b4d053151c22f80453d',
'ea17ac1d6225753c037b420a8f5de6b644f201348f8e50ddc257acacdeaad079',
'60a1fae1aaae32d2cba4a938b2c0bb33f8d2c60bca8b3fedbb8f56493d17ad76',
'7809e0d9a4bace42ac5d1a91cc8dc53285523dd38ba62c4a642ab0a28a514b2c',
'75b92543e587fa317d73b3eaf968ea605dc70b4e513e149e4f455dea39819a5b',
'84115c32bceeed4b10a08835b1038ff36b62a81e50105432192d4172f31bea3a',
'fdfefa96f9591f7f90cf4a6c31630826532a1f0d195a05c9bec604953736b8f9',
'875709a67b10cb2d2dde69fc0e29a851e618723bb5390cd0c37cd0de19a8dc8c',
'3fdf8b43d2e6eb59fc399f7cb1b81923d1dff0215d45a11e1c1f279827eaaad8',
'6c8e2759618e4965a6f0a6e5f71d8f628f139be0e187e6b61d9f497c38a74e2d',
'5763b253647027fe7f31ef93ae701ed563497a4f7ddeb611e1b872b3990a9466',
'00000061164ee0bf0c8ef6c236ebfa642929ebd47f7142d70a1835d26a348627',
'1e2e461c4b4033b5e01ca639c53fcf1c79b811c53efe51e9e01bfdacf8c541e5',
'cef69a32f8ab7e032f4c52e681876af87e7756a04bded1da05cd7eee4935f374',
'55b92d6c43896ef9e2ff48c8c55a804399b86ad8b3f84748c61d7a1d15d739f6',
'501b69b78a40e88073e9c1fd90cddf5d323350eec52e776117c4e778b9b504cc',
'6b165e83a3b7d333a6e7db1f200dc627e31eb5170285c29ded271be203c5da37',
'213eb4a024ff1c4523617be466dd4787c92ba9854010a6aefde948b8666d1fee',
'd1665cf52f5a9f27cf29703e05cc180c09b52aca9bb598af2c58c63e572bf387',
'1ef0953787a1cb0ff772aaf8b3c5548f31fa7dd1360fd85e69d78cdfbe4d1943',
'afb367080fffcd79ba47b20e36a8adf2030b01876c4eb23cfb2a0a879ad6d924',
'ba11337b58f9804a0f111016308ddac964267d86bea508eb33b36c7d9a1f8eb3',
'496d38f69865530028c7d212314d3ce6d605f3528a6c4020a067c9b5bc49fb13',
'c4d7a25e7f08aa2031512c21eaa323efe5eb02a350e42af4e3d7458712b52244',
'a59c614c81d3f597379ca5ea2ba5de6bec64660432f73ea36a8ed41b7e1e24b3',
'7c5f24e1c95f6f1f75555498f0019be1259a65c75ae851c235f7b15c9f88e0ee',
'3e1691aa75beb6aff2887e677b10f89a5ab9f71e7e3c54800eb6ab96d3afd9a7',
'830195f597a967fac5ebbce20b7775626d909172daa6e5f8ca7397b92421744e',
'754fcee48c504af2f4a3a47e218cb7beb92489f7297e9255222141e5838c2c69',
'e43f16ab84552a8680d3ade518803770fa16c9835da0a0f5b376cddef7f12786',
'4aebf76c348ace226a80999409b1d9043db6ac4dbddb7b127dcafcd18a722bf1',
'a8bf32d90f21bc06cd5631c2a1144d18a3c926e436badce43b943e53a25f4ca9',
'0000005f87f64341c212cc93d6c266c03ae752c02660e78a6da1424f7b05c470',
'0b453a2418397af510dae126c9aa0abb118aa559c42ed2faf5c4b98fd207e3c4',
'00000006db73c124c60c17a47bcd5f4212f790df6f9b14521d330ba697f3fc38',
'3c31c0c88fe605e342d792ef9ea1b1e27b61cd1127b825c8d9ad5088f01be8d1',
'b0c8af215af765a124c20c6e37b481df0b6c5e406f967c3d04216bb1843174a6',
'eead94ba213b3a8f764d88192ee6911340601d4cdb81aee6da3f4aad00c02078',
'36af487454212c5c815f04f7ca8be3e6e78ed3154bdf9cad80bcdb4dc0a9f78a',
'e7bf8dad360828f0289b7b4bea1a1bd28eb6d4d6522fa17f957e0dfb839ef3db',
'f03df3d4134230420cdf7acbb35f96a2542424246ab052ba24c6fec6a4d4f676',
'672c2d430e48294602416445452ef943feeb0c5ab4f0ec4469b605f8f2dd4c12',
'642317135fd4c4205323b9dea8af3270657e62d51dc31a657c0ec8aab31c6288',
'bfd121a37773554f2ef3e8e524e0e337f3bf852eb55cd9664e2c901514f063e7',
'd7c13d1edc3e0ba63ab74b859b2809fa15c0e8b538237dc8bd165b3f14cfe365',
'87e02cd9151cbf69ba20268a2a4237ad2f39fc631c96558e294ca00586477412',
'92f35ca35ddb1a4233c7c5d59e70e4412e039ed2775952734af7fd4b7bf6eb63']
]
]);
writeStdout(result);
}

129
tagPolicy.ts Executable file
View File

@ -0,0 +1,129 @@
import type { Policy } from '../types.ts';
/** Policy options for `hellthreadPolicy`. */
interface Hellthread {
/** Total number of "p" tags a kind 1 note may have before it's rejected. Default: `100` */
limit?: number;
}
/**
* Reject messages that tag too many participants.
*
* This policy is useful to prevent unwanted notifications by limiting the number of "p" tags a kind 1 event may have.
* Only kind 1 events are impacted by this policy, since kind 3 events will commonly exceed this number.
*
* @example
* ```ts
* // Reject events with more than 15 mentions.
* hellthreadPolicy(msg, { limit: 15 });
* ```
*/
const tagPolicy: Policy<Hellthread> = (msg, opts) => {
const limit = 0;
if (msg.event.kind === 4) {
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
if (msg.event.kind === 14) {
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
if (msg.event.kind === 0) {
return {
id: msg.event.id,
action: 'accept',
msg: '',
};
};
let mastodon: (string | number)[] = ['bae.st','bsky','liberal.city','mastodon.bot','botsin.space','a2mi.social','.au','masto.host','mastodon.online','social.beaware.live','nofan.xyz','mastodon.social','mstdn','mathstodon','universeodon','infosec', 'mastdn', 'kitty.social', 'c.im', '.jp', '.de', '.fr', 'toot', 'mastodon', 'misskey', 'journa.host', 'social', 'eldritchcafe', 'hachyderm', 'kinky.business']
const p = msg.event.tags.filter((tag) => tag[0] === 'p');
const e = msg.event.tags.filter((tag) => tag[0] === 'e');
const g = msg.event.tags.filter((tag) => tag[0] === 'g');
const a = msg.event.tags.filter((tag) => tag[0] === 'a');
const d = msg.event.tags.filter((tag) => tag[0] === 'd');
const h = msg.event.tags.filter((tag) => tag[0] === 'h');
const i = msg.event.tags.filter((tag) => tag[0] === 'i');
const k = msg.event.tags.filter((tag) => tag[0] === 'k');
const l = msg.event.tags.filter((tag) => tag[0] === 'l');
const q = msg.event.tags.filter((tag) => tag[0] === 'q');
const t = msg.event.tags.filter((tag) => tag[0] === 't');
const alt = msg.event.tags.filter((tag) => tag[0] === 'alt');
const content = msg.event.tags.filter((tag) => tag[0] === 'content-warning');
const image = msg.event.tags.filter((tag) => tag[0] === 'image');
const relay = msg.event.tags.filter((tag) => tag[0] === 'relay');
const client = msg.event.tags.filter((tag) => tag[0] === 'client');
const proxy = msg.event.tags.filter((tag) => tag[0] === 'proxy');
const blockheight = msg.event.tags.filter((tag) => tag[0] === 'blockheight');
/**
if (blockheight.length > limit) {
if (blockheight.length > limit) {
return {
id: msg.event.id,
action: 'reject',
msg: `Nostr Bots: BlockHeight`,
};
}
}
if (t.length > limit) {
if (t.length > limit) {
if (t.toString().indexOf('gnostr') > -1) {
return {
id: msg.event.id,
action: 'reject',
msg: `Nostr Bots: gnostr`,
};
}
}
}
**/
if (proxy.length > limit) {
for (let search of mastodon) {
if (proxy.length > limit) {
if(proxy.toString().indexOf('noauthority') > -1 || proxy.toString().indexOf('iddqd') > -1) {
return {
id: msg.event.id,
action: 'accept',
msg: `Fediverse Accept`,
};
}
if (proxy.toString().indexOf(search) > -1) {
return {
id: msg.event.id,
action: 'reject',
msg: `Fediverse Block: ` + search,
};
}
}
}
}
if (client.length > limit || d.length > limit || a.length > limit || h.length > limit || i.length > limit || k.length > limit || l.length > limit || q.length > limit || t.length > limit || alt.length > limit || content.length > limit || image.length > limit || relay.length > limit || g.length > limit || e.length > limit || p.length > limit) {
return {
id: msg.event.id,
action: 'accept',
msg: ``,
};
}
return {
id: msg.event.id,
action: 'reject',
msg: ``,
};
};
export default tagPolicy;
export type { TAG };