[
  {
    "path": ".github/workflows/publish-staging.yml",
    "content": "name: Publish Staging\n\non:\n  workflow_dispatch:\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    name: Deploy\n    steps:\n      - uses: actions/checkout@master\n      - name: Publish\n        env:\n          CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}\n        run: npm install -g wrangler && wrangler publish --env staging\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\n\non:\n  push:\n    branches:\n      - master\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    name: Deploy\n    steps:\n      - uses: actions/checkout@master\n      - name: Publish\n        env:\n          CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}\n        run: npm install -g wrangler && wrangler publish\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Tests\n\non:\n  pull_request:\n\njobs:\n  website:\n    name: Build WASM binary\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          components: clippy\n          target: wasm32-unknown-unknown\n          override: true\n          profile: minimal\n\n      - uses: jetli/wasm-pack-action@v0.4.0\n        with:\n          version: 'latest'\n\n      - uses: actions/cache@v2\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            target\n          key: cargo-${{ runner.os }}-browser-tests-${{ hashFiles('**/Cargo.toml') }}\n          restore-keys: |\n            cargo-${{ runner.os }}-browser-tests-\n            cargo-${{ runner.os }}-\n\n      - name: Build wasm\n        run: npm install -g wrangler && wrangler build\n\n  rust_tests:\n    name: Rust Checks\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          components: clippy, rustfmt\n          target: wasm32-unknown-unknown\n          override: true\n          profile: minimal\n\n      - name: Setup trunk\n        uses: jetli/trunk-action@v0.1.0\n        with:\n          version: 'latest'\n\n      - uses: actions/cache@v2\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            target\n          key: cargo-${{ runner.os }}-rust-tests-${{ hashFiles('**/Cargo.toml') }}\n          restore-keys: |\n            cargo-${{ runner.os }}-rust-tests-\n            cargo-${{ runner.os }}-\n\n      - name: Check formatting\n        working-directory: .\n        run: cargo fmt --check\n\n      - name: Check clippy \n        working-directory: .\n        run: cargo clippy \n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\nCargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# cloudflare things\n.DS_Store\n/node_modules\n\n**/*.rs.bk\nwasm-pack.log\n\nbuild/\n/target\n/dist\n.dev.vars\n.wrangler\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"blastr\"\nversion = \"0.0.0\"\nedition = \"2018\"\nresolver = \"2\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[features]\ndefault = [\"console_error_panic_hook\"]\n\n[dependencies]\ncfg-if = \"0.1.2\"\nworker = { version = \"0.0.18\", features = [\"queue\", \"d1\"] }\nfutures = \"0.3.26\"\nfutures-util = { version = \"0.3\", default-features = false }\nnostr = { version = \"0.22.0\", default-features = false, features = [\"nip11\"] }\nserde = { version = \"^1.0\", features = [\"derive\"] }\nserde_json = \"1.0.67\"\n\n# The `console_error_panic_hook` crate provides better debugging of panics by\n# logging them with `console.error`. This is great for development, but requires\n# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for\n# code size when deploying.\nconsole_error_panic_hook = { version = \"0.1.1\", optional = true }\n\n[profile.release]\n# Tell `rustc` to optimize for small code size.\nopt-level = \"s\"\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Mutiny Wallet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# blastr\n\nA nostr cloudflare workers proxy relay that publishes to all known online relays.\n\n![blastr diagram](./docs/images/blastr-diagram.png)\n\nThis takes advantage of the high availabilty of cloudflare serverless workers on the edge that are rust wasm-based with 0ms cold starts. Learn more about [cloudflare workers](https://workers.cloudflare.com/).\n\nThis is write only for now and is compatible with nostr clients but also features a simple POST api endpoint at `/event`. All events get queued up to run in batches by another worker that spins up every 30s if there's any events lined up, or once a certain amount of events are queued up.\n\nThis will help ensure that your events are broadcasted to as many places as possible.\n\n## Development\n\nWith `wrangler`, you can build, test, and deploy your Worker with the following commands:\n\n```sh\n# install wrangler if you do not have it yet\n$ npm install -g wrangler\n\n# log into cloudflare if you havent before\n$ wrangler login\n\n# compiles your project to WebAssembly and will warn of any issues\n$ npm run build\n\n# run your Worker in an ideal development workflow (with a local server, file watcher & more)\n$ npm run dev\n\n# deploy your Worker globally to the Cloudflare network (update your wrangler.toml file for configuration)\n$ npm run deploy\n```\n\n## Staging\n\nProduction deployments happen automatically but staging is manually for now. Deploy whenever you need to test changes before merging to master.\n\n```\nwrangler publish --env staging\n```\n\n### Setup\n\nThere's a few cloudflare components that Blastr uses behind the scenes, namely a KV store and multiple queues to distribute the load.\n\nRight now some of these are hardcoded for us since they have to map from the `wrangler.toml` file to the rust codebase. Need a TODO for making this more dynamic.\n\n#### KV store\n\nThis doesn't rebroadcast events that have already been broadcasted before. So we have a KV for that.\n\nWe also have a KV for storing the NWC requests and responses to get around ephemeral events.\n\n```\nwrangler kv:namespace create PUBLISHED_NOTES\nwrangler kv:namespace create PUBLISHED_NOTES --preview\n\nwrangler kv:namespace create NWC_REQUESTS\nwrangler kv:namespace create NWC_REQUESTS --preview\n\nwrangler kv:namespace create NWC_RESPONSES\nwrangler kv:namespace create NWC_RESPONSES --preview\n```\n\n#### Queues\n\n```\n wrangler queues create nostr-events-pub-1-b\n wrangler queues create nostr-events-pub-2-b\n wrangler queues create nostr-events-pub-3-b\n wrangler queues create nostr-events-pub-4-b\n wrangler queues create nostr-events-pub-5-b\n wrangler queues create nostr-events-pub-6-b\n wrangler queues create nostr-events-pub-7-b\n wrangler queues create nostr-events-pub-8-b\n wrangler queues create nostr-events-pub-9-b\n wrangler queues create nostr-events-pub-10-b\n```\n\nRead the latest `worker` crate documentation here: https://docs.rs/worker\n\n### CICD\n\nThere's an example workflow here for publishing on master branch pushes. You need to set `CF_API_TOKEN` in your github repo secrets first.\n\nYou also should either remove or configure `wrangler.toml` to point to a custom domain of yours:\n\n```\nroutes = [\n    { pattern = \"example.com/about\", zone_id = \"<YOUR_ZONE_ID>\" } # replace with your info\n]\n```\n\nand any other info in `wrangler.toml` that is custom to you, like the names / id's of queues or kv's.\n\n### WebAssembly\n\n`workers-rs` (the Rust SDK for Cloudflare Workers used in this template) is meant to be executed as compiled WebAssembly, and as such so **must** all the code you write and depend upon. All crates and modules used in Rust-based Workers projects have to compile to the `wasm32-unknown-unknown` triple.\n\nRead more about this on the [`workers-rs`](https://github.com/cloudflare/workers-rs) project README.\n"
  },
  {
    "path": "migrations/0000_events.sql",
    "content": "-- Migration number: 0000 \t 2023-08-10T16:43:44.275Z\nDROP TABLE IF EXISTS event;\n\nCREATE TABLE IF NOT EXISTS event (\n    id BLOB PRIMARY KEY,\n    created_at INTEGER NOT NULL,\n    pubkey BLOB NOT NULL,\n    kind INTEGER NOT NULL,\n    content TEXT NOT NULL,\n    sig BLOB NOT NULL,\n    deleted INTEGER NOT NULL DEFAULT 0\n);\n\nCREATE INDEX IF NOT EXISTS pubkey_index ON event(pubkey);\nCREATE INDEX IF NOT EXISTS kind_index ON event(kind);\n"
  },
  {
    "path": "migrations/0001_tag.sql",
    "content": "-- Migration number: 0001 \t 2023-08-11T20:22:11.351Z\nDROP TABLE IF EXISTS tag;\n\nCREATE TABLE IF NOT EXISTS tag (\n    id INTEGER PRIMARY KEY,\n    event_id BLOB NOT NULL,\n    name TEXT NOT NULL,\n    value TEXT,\n    FOREIGN KEY(event_id) REFERENCES event(id) ON DELETE CASCADE\n    UNIQUE(event_id, name, value)\n);\n\nCREATE INDEX IF NOT EXISTS tag_event_index ON tag(event_id);\nCREATE INDEX IF NOT EXISTS tag_name_index ON tag(name);\nCREATE INDEX IF NOT EXISTS tag_value_index ON tag(value);\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"private\": true,\n\t\"version\": \"0.0.0\",\n\t\"scripts\": {\n\t\t\"deploy\": \"wrangler publish\",\n\t\t\"dev\": \"wrangler dev --local\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@miniflare/tre\": \"^3.0.0-next.8\",\n\t\t\"wrangler\": \"^2.0.0\"\n\t}\n}\n"
  },
  {
    "path": "src/db.rs",
    "content": "use std::collections::HashMap;\n\nuse ::nostr::{Event, Kind, RelayMessage};\nuse nostr::{EventId, Tag, TagKind, Timestamp};\nuse serde::Deserialize;\nuse serde_json::Value;\nuse wasm_bindgen::prelude::JsValue;\nuse worker::D1Database;\nuse worker::*;\n\n#[derive(Debug, Deserialize)]\nstruct EventRow {\n    id: EventId,\n    pubkey: nostr::secp256k1::XOnlyPublicKey,\n    created_at: Timestamp,\n    kind: Kind,\n    tags: Option<Vec<Tag>>,\n    content: String,\n    sig: nostr::secp256k1::schnorr::Signature,\n}\n\n#[derive(Deserialize)]\nstruct TagRow {\n    event_id: EventId,\n    name: String,\n    value: String,\n}\n\npub async fn get_nwc_events(keys: &[String], kind: Kind, db: &D1Database) -> Result<Vec<Event>> {\n    // Determine the event kind\n    match kind {\n        Kind::WalletConnectResponse => (),\n        Kind::WalletConnectRequest => (),\n        _ => return Ok(vec![]), // skip other event types\n    };\n\n    console_log!(\"querying for ({keys:?}) and {}\", kind.as_u32());\n\n    // Query for the events first, without the tags\n    let placeholders: String = keys.iter().map(|_| \"?\").collect::<Vec<_>>().join(\", \");\n    let query_str = format!(\n        r#\"\n        SELECT * FROM event\n        WHERE pubkey IN ({}) AND kind = ? AND deleted = 0\n        \"#,\n        placeholders\n    );\n    let mut stmt = db.prepare(&query_str);\n    let mut bindings = Vec::with_capacity(keys.len() + 1); // +1 for the kind afterwards\n    for key in keys.iter() {\n        bindings.push(JsValue::from_str(key));\n    }\n    bindings.push(JsValue::from_f64(kind.as_u32() as f64));\n    stmt = stmt.bind(&bindings)?;\n\n    let result = stmt.all().await.map_err(|e| {\n        console_log!(\"Failed to fetch nwc events: {}\", e);\n        format!(\"Failed to fetch nwc events: {}\", e)\n    })?;\n\n    let mut events: Vec<Event> = result\n        .results::<Value>()?\n        .iter()\n        .map(|row| {\n            let e: EventRow = serde_json::from_value(row.clone()).map_err(|e| {\n                console_log!(\"failed to parse event: {}\", e);\n                worker::Error::from(format!(\n                    \"Failed to deserialize event from row ({}): {}\",\n                    row, e\n                ))\n            })?;\n            Ok(Event {\n                id: e.id,\n                pubkey: e.pubkey,\n                created_at: e.created_at,\n                kind: e.kind,\n                tags: e.tags.unwrap_or_default(),\n                content: e.content,\n                sig: e.sig,\n            })\n        })\n        .collect::<Result<Vec<Event>>>()?;\n\n    // Now get all the tags for all the events found\n    let event_ids: Vec<EventId> = events.iter().map(|e| e.id).collect();\n    let placeholders: String = event_ids.iter().map(|_| \"?\").collect::<Vec<_>>().join(\", \");\n    let tag_query_str = format!(\n        r#\"\n        SELECT event_id, name, value FROM tag\n        WHERE event_id IN ({})\n        ORDER BY id ASC\n        \"#,\n        placeholders\n    );\n    let mut tag_stmt = db.prepare(&tag_query_str);\n    let bindings: Vec<JsValue> = event_ids\n        .iter()\n        .map(|id| JsValue::from_str(&id.to_string()))\n        .collect();\n    tag_stmt = tag_stmt.bind(&bindings)?;\n\n    let tag_result = tag_stmt\n        .all()\n        .await\n        .map_err(|e| format!(\"Failed to fetch tags: {}\", e))?;\n\n    let tags: Vec<TagRow> = tag_result.results::<TagRow>()?;\n    let mut tags_map: HashMap<EventId, Vec<Tag>> = HashMap::new();\n\n    for tag_row in tags {\n        if let Ok(tag) = Tag::parse(vec![tag_row.name, tag_row.value]) {\n            tags_map\n                .entry(tag_row.event_id)\n                .or_insert_with(Vec::new)\n                .push(tag);\n        }\n    }\n\n    for event in &mut events {\n        if let Some(tags) = tags_map.remove(&event.id) {\n            event.tags.extend(tags);\n        }\n    }\n\n    // Tag ordering could screw up signature, though it shouldn't matter\n    // for NWC messages because those should only have one tag.\n    // Also we insert tags in order and do an ORDER BY id so it should be fine.\n    events.retain(|event| match event.verify() {\n        Ok(_) => true,\n        Err(e) => {\n            console_log!(\"Verification failed for event with id {}: {}\", event.id, e);\n            false\n        }\n    });\n\n    Ok(events)\n}\n\npub async fn handle_nwc_event(event: Event, db: &D1Database) -> Result<Option<RelayMessage>> {\n    // Determine the event kind\n    match event.kind {\n        Kind::WalletConnectResponse => (),\n        Kind::WalletConnectRequest => (),\n        _ => return Ok(None), // skip other event types\n    };\n\n    // Create the main event insertion query.\n    let event_insert_query = worker::query!(\n        db,\n        r#\"\n        INSERT OR IGNORE INTO event (id, created_at, pubkey, kind, content, sig)\n        VALUES (?, ?, ?, ?, ?, ?)\n        \"#,\n        &event.id,\n        &event.created_at,\n        &event.pubkey,\n        &event.kind,\n        &event.content,\n        &event.sig\n    )?;\n\n    // Create a vector of tag insertion queries.\n    let mut tag_insert_queries: Vec<_> = event\n        .tags\n        .iter()\n        .map(|tag| {\n            worker::query!(\n                db,\n                r#\"\n                INSERT OR IGNORE INTO tag (event_id, name, value)\n                VALUES (?, ?, ?)\n                \"#,\n                &event.id,\n                &tag.kind().to_string(),\n                &tag.as_vec().get(1)\n            )\n            .expect(\"should compile query\")\n        })\n        .collect();\n\n    // Combine the main event and tag insertion queries.\n    let mut batch_queries = vec![event_insert_query];\n    batch_queries.append(&mut tag_insert_queries);\n\n    // Run the batch queries.\n    let mut results = db.batch(batch_queries).await?.into_iter();\n\n    // Check the result of the main event insertion.\n    if let Some(error_msg) = results.next().and_then(|res| res.error()) {\n        console_log!(\"error saving nwc event to event table: {}\", error_msg);\n        let relay_msg = RelayMessage::new_ok(event.id, false, &error_msg);\n        return Ok(Some(relay_msg));\n    }\n\n    // Check the results for the tag insertions.\n    for tag_insert_result in results {\n        if let Some(error_msg) = tag_insert_result.error() {\n            console_log!(\"error saving tag to tag table: {}\", error_msg);\n            let relay_msg = RelayMessage::new_ok(event.id, false, &error_msg);\n            return Ok(Some(relay_msg));\n        }\n    }\n\n    let relay_msg = RelayMessage::new_ok(event.id, true, \"\");\n    Ok(Some(relay_msg))\n}\n\n/// When a NWC request has been fulfilled, soft delete the request from the database\npub async fn delete_nwc_request(event: Event, db: &D1Database) -> Result<()> {\n    // Filter only relevant events\n    match event.kind {\n        Kind::WalletConnectResponse => (),\n        _ => return Ok(()), // skip other event types\n    };\n\n    let p_tag = event.tags.iter().find(|t| t.kind() == TagKind::P).cloned();\n    let e_tag = event.tags.iter().find(|t| t.kind() == TagKind::E).cloned();\n\n    if let Some(Tag::PubKey(pubkey, ..)) = p_tag {\n        if let Some(Tag::Event(event_id, ..)) = e_tag {\n            // Soft delete the event based on pubkey and event_id\n            match worker::query!(\n                db,\n                \"UPDATE event SET deleted = 1 WHERE pubkey = ? AND id = ?\",\n                &pubkey.to_string(),\n                &event_id\n            )?\n            .run()\n            .await\n            {\n                Ok(_) => (),\n                Err(e) => {\n                    console_log!(\"error soft deleting nwc event from database: {e}\");\n                    return Ok(());\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// When a NWC response has been fulfilled, soft delete the response from the database\npub async fn delete_nwc_response(event: &Event, db: &D1Database) -> Result<()> {\n    // Filter only relevant events\n    match event.kind {\n        Kind::WalletConnectResponse => (),\n        _ => return Ok(()), // skip other event types\n    };\n\n    // Soft delete the event based on pubkey and id\n    match worker::query!(\n        db,\n        \"UPDATE event SET deleted = 1 WHERE pubkey = ? AND id = ?\",\n        &event.pubkey.to_string(),\n        &event.id\n    )?\n    .run()\n    .await\n    {\n        Ok(_) => console_log!(\"soft deleted nwc response event: {}\", event.id),\n        Err(e) => {\n            console_log!(\"error soft deleting nwc event from database: {e}\");\n            return Ok(());\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/error.rs",
    "content": "pub enum Error {\n    /// Worker error\n    WorkerError(String),\n}\n\nimpl From<worker::Error> for Error {\n    fn from(e: worker::Error) -> Self {\n        Error::WorkerError(e.to_string())\n    }\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "use crate::nostr::NOSTR_QUEUE_8;\nuse crate::nostr::NOSTR_QUEUE_9;\npub(crate) use crate::nostr::{\n    try_queue_event, NOSTR_QUEUE, NOSTR_QUEUE_2, NOSTR_QUEUE_3, NOSTR_QUEUE_4, NOSTR_QUEUE_5,\n    NOSTR_QUEUE_6,\n};\nuse crate::{db::delete_nwc_request, nostr::NOSTR_QUEUE_10};\nuse crate::{db::get_nwc_events, nostr::NOSTR_QUEUE_7};\nuse crate::{db::handle_nwc_event, nostr::get_nip11_response};\nuse ::nostr::{ClientMessage, Event, EventId, Filter, Kind, RelayMessage, SubscriptionId, Tag};\nuse futures::StreamExt;\nuse futures_util::lock::Mutex;\nuse serde::{Deserialize, Serialize};\nuse std::ops::DerefMut;\nuse std::rc::Rc;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::Arc;\nuse worker::*;\n\nmod db;\nmod error;\nmod nostr;\nmod utils;\n\nfn log_request(req: &Request) {\n    console_log!(\n        \"Incoming Request: {} - [{}]\",\n        Date::now().to_string(),\n        req.path(),\n    );\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct PublishedNote {\n    date: String,\n}\n\n/// The list of event kinds that are disallowed for the Nostr relay\n/// currently, this is just the NIP-95 event\nconst DISALLOWED_EVENT_KINDS: [u32; 1] = [1064];\n\n/// Main function for the Cloudflare Worker that triggers off of a HTTP req\n#[event(fetch)]\npub async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {\n    log_request(&req);\n\n    // Optionally, get more helpful error messages written to the console in the case of a panic.\n    utils::set_panic_hook();\n\n    // Optionally, use the Router to handle matching endpoints, use \":name\" placeholders, or \"*name\"\n    // catch-alls to match on specific patterns. Alternatively, use `Router::with_data(D)` to\n    // provide arbitrary data that will be accessible in each route via the `ctx.data()` method.\n    let router = Router::new();\n\n    // Add as many routes as your Worker needs! Each route will get a `Request` for handling HTTP\n    // functionality and a `RouteContext` which you can use to  and get route parameters and\n    // Environment bindings like KV Stores, Durable Objects, Secrets, and Variables.\n    router\n        .post_async(\"/event\", |mut req, ctx| async move {\n            // for any adhoc POST event\n            match req.text().await {\n                Ok(request_text) => {\n                    if let Ok(client_msg) = ClientMessage::from_json(request_text) {\n                        match client_msg {\n                            ClientMessage::Event(event) => {\n                                console_log!(\"got an event from client: {}\", event.id);\n\n                                match event.verify() {\n                                    Ok(()) => (),\n                                    Err(e) => {\n                                        console_log!(\"could not verify event {}: {}\", event.id, e);\n                                        let relay_msg =\n                                            RelayMessage::new_ok(event.id, false, \"invalid event\");\n                                        return relay_response(relay_msg);\n                                    }\n                                }\n\n                                // check if disallowed event kind\n                                if DISALLOWED_EVENT_KINDS.contains(&event.kind.as_u32()) {\n                                    console_log!(\n                                        \"invalid event kind {}: {}\",\n                                        event.kind.as_u32(),\n                                        event.id\n                                    );\n                                    let relay_msg = RelayMessage::new_ok(\n                                        event.id,\n                                        false,\n                                        \"disallowed event kind\",\n                                    );\n                                    return relay_response(relay_msg);\n                                };\n\n                                // check if we've already published it before\n                                let published_notes = ctx.kv(\"PUBLISHED_NOTES\")?;\n                                if published_notes\n                                    .get(event.id.to_string().as_str())\n                                    .json::<PublishedNote>()\n                                    .await\n                                    .ok()\n                                    .flatten()\n                                    .is_some()\n                                {\n                                    console_log!(\"event already published: {}\", event.id);\n                                    let relay_msg = RelayMessage::new_ok(\n                                        event.id,\n                                        false,\n                                        \"event already published\",\n                                    );\n                                    return relay_response(relay_msg);\n                                };\n\n                                let db = ctx.d1(\"DB\")?;\n\n                                // if the event is a nostr wallet connect event, we\n                                // should save it and not send to other relays.\n                                if let Some(relay_msg) =\n                                    handle_nwc_event(*event.clone(), &db).await?\n                                {\n                                    if let Err(e) = delete_nwc_request(*event, &db).await {\n                                        console_log!(\"failed to delete nwc request: {e}\");\n                                    }\n                                    return relay_response(relay_msg);\n                                };\n\n                                // broadcast it to all queues\n                                let nostr_queues = vec![\n                                    ctx.env.queue(NOSTR_QUEUE).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_2).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_3).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_4).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_5).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_6).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_7).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_8).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_9).expect(\"get queue\"),\n                                    ctx.env.queue(NOSTR_QUEUE_10).expect(\"get queue\"),\n                                ];\n                                try_queue_event(*event.clone(), nostr_queues).await;\n                                console_log!(\"queued up nostr event: {}\", event.id);\n                                match published_notes\n                                    .put(\n                                        event.id.to_string().as_str(),\n                                        PublishedNote {\n                                            date: Date::now().to_string(),\n                                        },\n                                    )?\n                                    .execute()\n                                    .await\n                                {\n                                    Ok(_) => {\n                                        console_log!(\"saved published note: {}\", event.id);\n                                        let relay_msg = RelayMessage::new_ok(event.id, true, \"\");\n                                        relay_response(relay_msg)\n                                    }\n                                    Err(e) => {\n                                        console_log!(\n                                            \"could not save published note: {} - {e:?}\",\n                                            event.id\n                                        );\n                                        let relay_msg = RelayMessage::new_ok(\n                                            event.id,\n                                            false,\n                                            \"error: could not save published note\",\n                                        );\n                                        relay_response(relay_msg)\n                                    }\n                                }\n                            }\n                            _ => {\n                                console_log!(\"ignoring other nostr client message types\");\n                                Response::error(\"Only Event types allowed\", 400)\n                            }\n                        }\n                    } else {\n                        Response::error(\"Could not parse Client Message\", 400)\n                    }\n                }\n                Err(e) => {\n                    console_log!(\"could not get request text: {}\", e);\n                    Response::error(\"Could not get request text\", 400)\n                }\n            }\n        })\n        .get(\"/\", |req, ctx| {\n            // NIP 11\n            if req.headers().get(\"Accept\").ok().flatten()\n                == Some(\"application/nostr+json\".to_string())\n            {\n                return Response::from_json(&get_nip11_response())?.with_cors(&cors());\n            }\n\n            let ctx = Rc::new(ctx);\n            // For websocket compatibility\n            let pair = WebSocketPair::new()?;\n            let server = pair.server;\n            server.accept()?;\n            console_log!(\"accepted websocket, about to spawn event stream\");\n            wasm_bindgen_futures::spawn_local(async move {\n                let running_thread = Arc::new(AtomicBool::new(false));\n                let new_subscription_req = Arc::new(AtomicBool::new(false));\n                let requested_filters = Arc::new(Mutex::new(Filter::new()));\n                let mut event_stream = server.events().expect(\"stream error\");\n                console_log!(\"spawned event stream, waiting for first message..\");\n                while let Some(event) = event_stream.next().await {\n                    if let Err(e) = event {\n                        console_log!(\"error parsing some event: {e}\");\n                        continue;\n                    }\n                    match event.expect(\"received error in websocket\") {\n                        WebsocketEvent::Message(msg) => {\n                            if msg.text().is_none() {\n                                continue;\n                            };\n                            if let Ok(client_msg) = ClientMessage::from_json(msg.text().unwrap()) {\n                                match client_msg {\n                                    ClientMessage::Event(event) => {\n                                        console_log!(\"got an event from client: {}\", event.id);\n                                        match event.verify() {\n                                            Ok(()) => (),\n                                            Err(e) => {\n                                                console_log!(\n                                                    \"could not verify event {}: {}\",\n                                                    event.id,\n                                                    e\n                                                );\n                                                let relay_msg = RelayMessage::new_ok(\n                                                    event.id,\n                                                    false,\n                                                    \"disallowed event kind\",\n                                                );\n                                                server\n                                                    .send_with_str(&relay_msg.as_json())\n                                                    .expect(\"failed to send response\");\n                                                continue;\n                                            }\n                                        }\n\n                                        // check if disallowed event kind\n                                        if DISALLOWED_EVENT_KINDS.contains(&event.kind.as_u32()) {\n                                            console_log!(\n                                                \"invalid event kind {}: {}\",\n                                                event.kind.as_u32(),\n                                                event.id\n                                            );\n                                            let relay_msg = RelayMessage::new_ok(\n                                                event.id,\n                                                false,\n                                                \"disallowed event kind\",\n                                            );\n                                            server\n                                                .send_with_str(&relay_msg.as_json())\n                                                .expect(\"failed to send response\");\n                                            continue;\n                                        };\n\n                                        // check if we've already published it before\n                                        let published_notes =\n                                            ctx.kv(\"PUBLISHED_NOTES\").expect(\"get kv\");\n                                        if published_notes\n                                            .get(event.id.to_string().as_str())\n                                            .json::<PublishedNote>()\n                                            .await\n                                            .ok()\n                                            .flatten()\n                                            .is_some()\n                                        {\n                                            console_log!(\"event already published: {}\", event.id);\n                                            let relay_msg = RelayMessage::new_ok(\n                                                event.id,\n                                                false,\n                                                \"event already published\",\n                                            );\n                                            server\n                                                .send_with_str(&relay_msg.as_json())\n                                                .expect(\"failed to send response\");\n                                            continue;\n                                        };\n\n                                        let db = ctx.d1(\"DB\").expect(\"should have DB\");\n\n                                        // if the event is a nostr wallet connect event, we\n                                        // should save it and not send to other relays.\n                                        if let Some(response) =\n                                            handle_nwc_event(*event.clone(), &db)\n                                                .await\n                                                .expect(\"failed to handle nwc event\")\n                                        {\n                                            server\n                                                .send_with_str(&response.as_json())\n                                                .expect(\"failed to send response\");\n\n                                            if let Err(e) = delete_nwc_request(*event, &db).await {\n                                                console_log!(\"failed to delete nwc request: {e}\");\n                                            }\n\n                                            continue;\n                                        };\n\n                                        // broadcast it to all queues\n                                        let nostr_queues = vec![\n                                            ctx.env.queue(NOSTR_QUEUE).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_2).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_3).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_4).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_5).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_6).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_7).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_8).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_9).expect(\"get queue\"),\n                                            ctx.env.queue(NOSTR_QUEUE_10).expect(\"get queue\"),\n                                        ];\n                                        try_queue_event(*event.clone(), nostr_queues).await;\n                                        console_log!(\"queued up nostr event: {}\", event.id);\n                                        match published_notes\n                                            .put(\n                                                event.id.to_string().as_str(),\n                                                PublishedNote {\n                                                    date: Date::now().to_string(),\n                                                },\n                                            )\n                                            .expect(\"saved note\")\n                                            .execute()\n                                            .await\n                                        {\n                                            Ok(_) => {\n                                                console_log!(\"saved published note: {}\", event.id);\n                                            }\n                                            Err(e) => {\n                                                console_log!(\n                                                    \"could not save published note: {} - {e:?}\",\n                                                    event.id\n                                                );\n                                            }\n                                        }\n\n                                        let relay_msg = RelayMessage::new_ok(event.id, true, \"\");\n                                        server\n                                            .send_with_str(&relay_msg.as_json())\n                                            .expect(\"failed to send response\");\n                                    }\n                                    ClientMessage::Req {\n                                        subscription_id,\n                                        filters,\n                                    } => {\n                                        new_subscription_req.swap(true, Ordering::Relaxed);\n                                        console_log!(\n                                            \"got a new client request sub: {}, len: {}\",\n                                            subscription_id,\n                                            filters.len()\n                                        );\n                                        // for each filter we handle it every 10 seconds\n                                        // by reading storage and sending any new events\n                                        // one caveat is that this will send events multiple\n                                        // times if they are in multiple filters\n                                        let mut valid = false;\n                                        for filter in filters {\n                                            let valid_nwc = {\n                                                let nwc_kinds = filter\n                                                    .kinds\n                                                    .as_ref()\n                                                    .map(|k| {\n                                                        k.contains(&Kind::WalletConnectResponse)\n                                                            || k.contains(\n                                                                &Kind::WalletConnectRequest,\n                                                            )\n                                                    })\n                                                    .unwrap_or(false);\n\n                                                let has_authors = filter\n                                                    .authors\n                                                    .as_ref()\n                                                    .map(|a| !a.is_empty())\n                                                    .unwrap_or(false);\n                                                let has_pks = filter\n                                                    .pubkeys\n                                                    .as_ref()\n                                                    .map(|a| !a.is_empty())\n                                                    .unwrap_or(false);\n\n                                                nwc_kinds && (has_authors || has_pks)\n                                            };\n\n                                            if valid_nwc {\n                                                let mut master_guard =\n                                                    requested_filters.lock().await;\n                                                let master_filter = master_guard.deref_mut();\n                                                // now add the new filters to the main filter\n                                                // object. This is a bit of a hack but we only\n                                                // check certain sub filters for NWC.\n                                                combine_filters(master_filter, &filter);\n                                                console_log!(\n                                                    \"New filter count: {}\",\n                                                    master_filter\n                                                        .pubkeys\n                                                        .as_ref()\n                                                        .map_or(0, Vec::len)\n                                                );\n                                                valid = true;\n                                            }\n                                        }\n\n                                        // only spin up a new one if there's not a\n                                        // spawn_local already going with filters\n                                        // when other filters are added in, it should\n                                        // be picked up in the master filter\n                                        let mut sent_event_count = 0;\n                                        if !running_thread.load(Ordering::Relaxed) && valid {\n                                            // set running thread to true\n                                            running_thread.swap(true, Ordering::Relaxed);\n\n                                            let db = ctx.d1(\"DB\").expect(\"should have DB\");\n                                            let sub_id = subscription_id.clone();\n                                            let server_clone = server.clone();\n                                            let master_clone = requested_filters.clone();\n                                            let new_subscription_req_clone =\n                                                new_subscription_req.clone();\n                                            wasm_bindgen_futures::spawn_local(async move {\n                                                let mut sent_events = vec![];\n                                                loop {\n                                                    let master = master_clone.lock().await;\n                                                    console_log!(\n                                                        \"Checking filters: {}\",\n                                                        master.pubkeys.as_ref().map_or(0, Vec::len)\n                                                    );\n                                                    match handle_filter(\n                                                        &sent_events,\n                                                        sub_id.clone(),\n                                                        master.clone(),\n                                                        &server_clone,\n                                                        &db,\n                                                    )\n                                                    .await\n                                                    {\n                                                        Ok(new_event_ids) => {\n                                                            // add new events to sent events\n                                                            sent_events.extend(new_event_ids);\n                                                            // send EOSE if necessary\n                                                            if new_subscription_req_clone\n                                                                .load(Ordering::Relaxed)\n                                                                || sent_event_count\n                                                                    != sent_events.len()\n                                                            {\n                                                                let relay_msg =\n                                                                    RelayMessage::new_eose(\n                                                                        sub_id.clone(),\n                                                                    );\n                                                                server_clone\n                                                                    .send_with_str(\n                                                                        relay_msg.as_json(),\n                                                                    )\n                                                                    .expect(\n                                                                        \"failed to send response\",\n                                                                    );\n                                                                sent_event_count =\n                                                                    sent_events.len();\n                                                                new_subscription_req_clone\n                                                                    .swap(false, Ordering::Relaxed);\n                                                            }\n                                                        }\n                                                        Err(e) => console_log!(\n                                                            \"error handling filter: {e}\"\n                                                        ),\n                                                    }\n                                                    drop(master);\n                                                    utils::delay(5_000).await;\n                                                }\n                                            });\n                                        } else if !valid {\n                                            // if not a nwc filter, we just send EOSE\n                                            let relay_msg = RelayMessage::new_eose(subscription_id);\n                                            server\n                                                .send_with_str(relay_msg.as_json())\n                                                .expect(\"failed to send response\");\n                                        }\n                                    }\n                                    _ => {\n                                        console_log!(\"ignoring other nostr client message types\");\n                                    }\n                                }\n                            }\n                        }\n                        WebsocketEvent::Close(_) => {\n                            console_log!(\"closing\");\n                            break;\n                        }\n                    }\n                }\n            });\n            Response::from_websocket(pair.client)\n        })\n        .get(\"/favicon.ico\", |_, _| {\n            let bytes: Vec<u8> = include_bytes!(\"../static/favicon.ico\").to_vec();\n            Response::from_bytes(bytes)?.with_cors(&cors())\n        })\n        .options(\"/*catchall\", |_, _| empty_response())\n        .run(req, env)\n        .await\n}\n\n/// Main function for the Cloudflare Worker that triggers off the nostr event queue\n#[event(queue)]\npub async fn main(message_batch: MessageBatch<Event>, _env: Env, _ctx: Context) -> Result<()> {\n    // Deserialize the message batch\n    let messages: Vec<Message<Event>> = message_batch.messages()?;\n    let mut events: Vec<Event> = messages.iter().map(|m| m.body.clone()).collect();\n    events.sort();\n    events.dedup();\n\n    let part = queue_number(message_batch.queue().as_str())?;\n    match nostr::send_nostr_events(events, part).await {\n        Ok(event_ids) => {\n            for event_id in event_ids {\n                console_log!(\"Sent nostr event: {}\", event_id)\n            }\n        }\n        Err(error::Error::WorkerError(e)) => {\n            console_log!(\"worker error: {e}\");\n        }\n    }\n\n    Ok(())\n}\n\npub fn queue_number(batch_name: &str) -> Result<u32> {\n    match batch_name {\n        NOSTR_QUEUE => Ok(0),\n        NOSTR_QUEUE_2 => Ok(1),\n        NOSTR_QUEUE_3 => Ok(2),\n        NOSTR_QUEUE_4 => Ok(3),\n        NOSTR_QUEUE_5 => Ok(4),\n        NOSTR_QUEUE_6 => Ok(5),\n        NOSTR_QUEUE_7 => Ok(6),\n        NOSTR_QUEUE_8 => Ok(7),\n        NOSTR_QUEUE_9 => Ok(8),\n        NOSTR_QUEUE_10 => Ok(9),\n        _ => Err(\"unexpected queue\".into()),\n    }\n}\n\n/// if the user requests a NWC event, we have those stored,\n/// we should send them to the user\npub async fn handle_filter(\n    sent_events: &[EventId],\n    subscription_id: SubscriptionId,\n    filter: Filter,\n    server: &WebSocket,\n    db: &D1Database,\n) -> Result<Vec<EventId>> {\n    let mut events = vec![];\n    // get all authors and pubkeys\n    let mut keys = filter.authors.unwrap_or_default();\n    keys.extend(\n        filter\n            .pubkeys\n            .unwrap_or_default()\n            .into_iter()\n            .map(|p| p.to_string()),\n    );\n\n    if filter\n        .kinds\n        .clone()\n        .unwrap_or_default()\n        .contains(&Kind::WalletConnectRequest)\n    {\n        let mut found_events = get_nwc_events(&keys, Kind::WalletConnectRequest, db)\n            .await\n            .unwrap_or_default();\n\n        // filter out events that have already been sent\n        found_events.retain(|e| !sent_events.contains(&e.id));\n\n        events.extend(found_events);\n    }\n\n    if filter\n        .kinds\n        .unwrap_or_default()\n        .contains(&Kind::WalletConnectResponse)\n    {\n        let mut found_events = get_nwc_events(&keys, Kind::WalletConnectResponse, db)\n            .await\n            .unwrap_or_default();\n\n        // filter out events that have already been sent\n        found_events.retain(|e| !sent_events.contains(&e.id));\n\n        events.extend(found_events);\n    }\n\n    // if the filter is only requesting replies to a certain event filter to only those\n    if let Some(event_ids) = filter.events {\n        events.retain(|event| {\n            event\n                .tags\n                .iter()\n                .any(|t| matches!(t, Tag::Event(id, _, _) if event_ids.contains(id)))\n        })\n    }\n\n    if events.is_empty() {\n        return Ok(vec![]);\n    }\n\n    if !events.is_empty() {\n        // sort and dedup events\n        events.sort_by(|a, b| a.created_at.cmp(&b.created_at));\n        events.dedup();\n\n        // send all found events to the user\n        for event in events.clone() {\n            console_log!(\"sending event to client: {}\", &event.id);\n            let relay_msg = RelayMessage::new_event(subscription_id.clone(), event);\n            server\n                .send_with_str(&relay_msg.as_json())\n                .expect(\"failed to send response\");\n        }\n    }\n\n    let sent_event_ids: Vec<EventId> = events.into_iter().map(|e| e.id).collect();\n    Ok(sent_event_ids)\n}\n\n// Helper function to extend a vector without duplicates\nfn extend_without_duplicates<T: PartialEq + Clone>(master: &mut Vec<T>, new: &Vec<T>) {\n    for item in new {\n        if !master.contains(item) {\n            master.push(item.clone());\n        }\n    }\n}\n\nfn combine_filters(master_filter: &mut Filter, new_filter: &Filter) {\n    // Check and extend for IDs\n    if let Some(vec) = &new_filter.ids {\n        let master_vec = master_filter.ids.get_or_insert_with(Vec::new);\n        extend_without_duplicates(master_vec, vec);\n    }\n\n    // Check and extend for authors\n    if let Some(vec) = &new_filter.authors {\n        let master_vec = master_filter.authors.get_or_insert_with(Vec::new);\n        extend_without_duplicates(master_vec, vec);\n    }\n\n    // Check and extend for kinds\n    if let Some(vec) = &new_filter.kinds {\n        let master_vec = master_filter.kinds.get_or_insert_with(Vec::new);\n        extend_without_duplicates(master_vec, vec);\n    }\n\n    // Check and extend for events\n    if let Some(vec) = &new_filter.events {\n        let master_vec = master_filter.events.get_or_insert_with(Vec::new);\n        extend_without_duplicates(master_vec, vec);\n    }\n\n    // Check and extend for pubkeys\n    if let Some(vec) = &new_filter.pubkeys {\n        let master_vec = master_filter.pubkeys.get_or_insert_with(Vec::new);\n        extend_without_duplicates(master_vec, vec);\n    }\n\n    // Check and extend for hashtags\n    if let Some(vec) = &new_filter.hashtags {\n        let master_vec = master_filter.hashtags.get_or_insert_with(Vec::new);\n        extend_without_duplicates(master_vec, vec);\n    }\n\n    // Check and extend for references\n    if let Some(vec) = &new_filter.references {\n        let master_vec = master_filter.references.get_or_insert_with(Vec::new);\n        extend_without_duplicates(master_vec, vec);\n    }\n\n    // Check and extend for identifiers\n    if let Some(vec) = &new_filter.identifiers {\n        let master_vec = master_filter.identifiers.get_or_insert_with(Vec::new);\n        extend_without_duplicates(master_vec, vec);\n    }\n}\n\nfn relay_response(msg: RelayMessage) -> worker::Result<Response> {\n    Response::from_json(&msg)?.with_cors(&cors())\n}\n\nfn empty_response() -> worker::Result<Response> {\n    Response::empty()?.with_cors(&cors())\n}\n\nfn cors() -> Cors {\n    Cors::new()\n        .with_credentials(true)\n        .with_origins(vec![\"*\"])\n        .with_allowed_headers(vec![\"Content-Type\"])\n        .with_methods(Method::all())\n}\n"
  },
  {
    "path": "src/nostr.rs",
    "content": "use crate::error::Error;\nuse crate::utils::delay;\nuse futures::future::Either;\nuse futures::pin_mut;\nuse futures::StreamExt;\nuse nostr::prelude::*;\nuse std::string::ToString;\nuse std::vec;\nuse worker::WebsocketEvent;\nuse worker::{console_log, Cache, Fetch, Queue, Response, WebSocket};\n\npub(crate) const NOSTR_QUEUE: &str = \"nostr-events-pub-1-b\";\npub(crate) const NOSTR_QUEUE_2: &str = \"nostr-events-pub-2-b\";\npub(crate) const NOSTR_QUEUE_3: &str = \"nostr-events-pub-3-b\";\npub(crate) const NOSTR_QUEUE_4: &str = \"nostr-events-pub-4-b\";\npub(crate) const NOSTR_QUEUE_5: &str = \"nostr-events-pub-5-b\";\npub(crate) const NOSTR_QUEUE_6: &str = \"nostr-events-pub-6-b\";\npub(crate) const NOSTR_QUEUE_7: &str = \"nostr-events-pub-7-b\";\npub(crate) const NOSTR_QUEUE_8: &str = \"nostr-events-pub-8-b\";\npub(crate) const NOSTR_QUEUE_9: &str = \"nostr-events-pub-9-b\";\npub(crate) const NOSTR_QUEUE_10: &str = \"nostr-events-pub-10-b\";\nconst RELAY_LIST_URL: &str = \"https://api.nostr.watch/v1/online\";\nconst RELAYS: [&str; 8] = [\n    \"wss://nostr.zebedee.cloud\",\n    \"wss://relay.snort.social\",\n    \"wss://eden.nostr.land\",\n    \"wss://nos.lol\",\n    \"wss://brb.io\",\n    \"wss://nostr.fmt.wiz.biz\",\n    \"wss://relay.damus.io\",\n    \"wss://nostr.wine\",\n];\n\npub fn get_nip11_response() -> RelayInformationDocument {\n    let version = env!(\"CARGO_PKG_VERSION\");\n    let supported_nips = vec![1, 11, 15, 20];\n\n    RelayInformationDocument {\n        name: Some(\"Mutiny blastr relay\".to_string()),\n        description: Some(\"Mutiny blastr relay\".to_string()),\n        pubkey: Some(\n            \"df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708\".to_string(),\n        ),\n        contact: Some(\"team@mutinywallet.com\".to_string()),\n        supported_nips: Some(supported_nips),\n        software: Some(\"git+https://github.com/MutinyWallet/blastr.git\".to_string()),\n        version: Some(version.to_string()),\n    }\n}\n\npub async fn try_queue_event(event: Event, nostr_queues: Vec<Queue>) {\n    for nostr_queue in nostr_queues.iter() {\n        match queue_nostr_event_with_queue(nostr_queue, event.clone()).await {\n            Ok(_) => {}\n            Err(Error::WorkerError(e)) => {\n                console_log!(\"worker error: {e}\");\n            }\n        }\n    }\n}\n\npub async fn queue_nostr_event_with_queue(nostr_queue: &Queue, event: Event) -> Result<(), Error> {\n    nostr_queue.send(&event).await?;\n    Ok(())\n}\n\nasync fn send_event_to_relay(messages: Vec<ClientMessage>, relay: &str) -> Result<(), Error> {\n    // skip self\n    if relay == \"wss://nostr.mutinywallet.com\" {\n        return Ok(());\n    }\n    let connect_timeout = delay(10_000);\n    let connect_future = WebSocket::connect(relay.parse().unwrap());\n\n    pin_mut!(connect_timeout);\n    pin_mut!(connect_future);\n\n    let either = futures::future::select(connect_future, connect_timeout).await;\n\n    let ws_opt = match either {\n        Either::Left((res, _)) => res,\n        Either::Right(_) => {\n            console_log!(\"time delay hit, stopping...\");\n            Err(worker::Error::RustError(\"Connection timeout\".to_string()))\n        }\n    };\n\n    match ws_opt {\n        Ok(ws) => {\n            // It's important that we call this before we send our first message, otherwise we will\n            // not have any event listeners on the socket to receive the echoed message.\n            let event_stream = ws.events();\n            if let Some(e) = event_stream.as_ref().err() {\n                console_log!(\"Error calling ws events from relay {relay}: {e:?}\");\n                return Err(Error::WorkerError(String::from(\"WS connection error\")));\n            }\n            let mut event_stream = event_stream.unwrap();\n\n            if let Some(e) = ws.accept().err() {\n                console_log!(\"Error accepting ws from relay {relay}: {e:?}\");\n                return Err(e.into());\n            }\n\n            for message in messages {\n                if let Some(e) = ws.send_with_str(message.as_json()).err() {\n                    console_log!(\"Error sending event to relay {relay}: {e:?}\")\n                }\n            }\n\n            // log the first message we received from the relay\n            let sleep = delay(1_000);\n            let event_stream_fut = event_stream.next();\n            pin_mut!(event_stream_fut);\n            pin_mut!(sleep);\n            match futures::future::select(event_stream_fut, sleep).await {\n                Either::Left((event_stream_res, _)) => {\n                    if let Some(event) = event_stream_res {\n                        let event = event?;\n\n                        if let WebsocketEvent::Message(msg) = event {\n                            if let Some(text) = msg.text() {\n                                console_log!(\"websocket event from relay {relay}: {text}\")\n                            }\n                        }\n                    }\n                }\n                Either::Right(_) => {\n                    console_log!(\"time delay hit, stopping...\");\n                    // Sleep triggered before we got a websocket response\n                }\n            }\n\n            if let Some(_e) = ws.close::<String>(None, None).err() {\n                console_log!(\"Error websocket to relay {relay}\")\n            }\n        }\n        Err(e) => {\n            console_log!(\"Error connecting to relay {relay}: {e:?}\");\n            return Err(Error::WorkerError(String::from(\"WS connection error\")));\n        }\n    };\n\n    Ok(())\n}\n\npub async fn send_nostr_events(events: Vec<Event>, part: u32) -> Result<Vec<EventId>, Error> {\n    let messages: Vec<ClientMessage> = events\n        .iter()\n        .map(|e| ClientMessage::new_event(e.clone()))\n        .collect();\n\n    // pull in the relays from nostr watch list\n    let cache = Cache::default();\n    let relays = if let Some(mut resp) = cache.get(RELAY_LIST_URL, true).await? {\n        console_log!(\"cache hit for relays\");\n        match resp.json::<Vec<String>>().await {\n            Ok(r) => r,\n            Err(_) => RELAYS.iter().map(|x| x.to_string()).collect(),\n        }\n    } else {\n        console_log!(\"no cache hit for relays\");\n        match Fetch::Url(RELAY_LIST_URL.parse().unwrap()).send().await {\n            Ok(mut nostr_resp) => {\n                console_log!(\"retrieved online relay list\");\n                match nostr_resp.json::<Vec<String>>().await {\n                    Ok(r) => {\n                        let mut resp = Response::from_json(&r)?;\n\n                        // Cache API respects Cache-Control headers. Setting s-max-age to 10\n                        // will limit the response to be in cache for 10 seconds max\n                        resp.headers_mut().set(\"cache-control\", \"s-maxage=1800\")?;\n                        cache.put(RELAY_LIST_URL, resp.cloned()?).await?;\n                        match resp.json::<Vec<String>>().await {\n                            Ok(r) => r,\n                            Err(e) => {\n                                console_log!(\"could not parse nostr relay list json: {}\", e);\n                                RELAYS.iter().map(|x| x.to_string()).collect()\n                            }\n                        }\n                    }\n                    Err(e) => {\n                        console_log!(\"could not parse nostr relay list response: {}\", e);\n                        RELAYS.iter().map(|x| x.to_string()).collect()\n                    }\n                }\n            }\n            Err(e) => {\n                console_log!(\"could not retrieve relay list: {}\", e);\n                RELAYS.iter().map(|x| x.to_string()).collect()\n            }\n        }\n    };\n    // find range of elements for this part\n    let sub_relays = get_sub_vec_range(relays, find_range_from_part(part));\n    let mut futures = Vec::new();\n    for relay in sub_relays.iter() {\n        let fut = send_event_to_relay(messages.clone(), relay);\n        futures.push(fut);\n    }\n    let combined_futures = futures::future::join_all(futures);\n    let sleep = delay(120_000);\n    pin_mut!(combined_futures);\n    pin_mut!(sleep);\n    futures::future::select(combined_futures, sleep).await;\n    Ok(events.iter().map(|e| e.id).collect())\n}\n\nfn get_sub_vec_range(original: Vec<String>, range: (usize, usize)) -> Vec<String> {\n    let len = original.len();\n    if range.0 >= len {\n        return vec![];\n    }\n    let end = if range.1 >= len { len - 1 } else { range.1 };\n    original[range.0..end].to_vec()\n}\n\nfn find_range_from_part(part: u32) -> (usize, usize) {\n    let start = 30 * part;\n    let end = start + 29;\n    (start as usize, end as usize)\n}\n"
  },
  {
    "path": "src/utils.rs",
    "content": "use cfg_if::cfg_if;\nuse std::time::Duration;\nuse worker::Delay;\n\ncfg_if! {\n    // https://github.com/rustwasm/console_error_panic_hook#readme\n    if #[cfg(feature = \"console_error_panic_hook\")] {\n        extern crate console_error_panic_hook;\n        pub use self::console_error_panic_hook::set_once as set_panic_hook;\n    } else {\n        #[inline]\n        pub fn set_panic_hook() {}\n    }\n}\n\npub async fn delay(delay: u64) {\n    let delay: Delay = Duration::from_millis(delay).into();\n    delay.await;\n}\n"
  },
  {
    "path": "wrangler.toml",
    "content": "name = \"blastr\"\nmain = \"build/worker/shim.mjs\"\ncompatibility_date = \"2022-01-20\"\n\n# replace with your domain info - TODO this might not be required but we added it for ours.\nroutes = [\n    { pattern = \"nostr.mutinywallet.com/\", zone_id = \"2b9268714ce8d1c4431e8046d4ba55d3\" },\n    { pattern = \"nostr.mutinywallet.com/event\", zone_id = \"2b9268714ce8d1c4431e8046d4ba55d3\" }\n]\n\n# replace with your KV store info\n# create the queues with `wrangler kv:namespace create PUBLISHED_NOTES` and the same command with the `--preview` flag.\n# put your queue IDs below\nkv_namespaces = [\n  { binding = \"PUBLISHED_NOTES\", id = \"afa24a392a5a41f6b1655507dfd9b97a\", preview_id = \"0b334aece8d74c3ab90e3e99db569ce8\" },\n  { binding = \"NWC_REQUESTS\", id = \"e5bd788ddc16410bb108df0f2ae89e62\", preview_id = \"63d99f551a464ff78bbbbee7113cb658\" },\n  { binding = \"NWC_RESPONSES\", id = \"5b434d5eced84abaad1c9a44448ac71c\", preview_id = \"af27b55b58754562b4250dcd3682547b\" },\n]\n\n[env.staging]\nname = \"blastr-staging\"\nroutes = [\n    { pattern = \"nostr-staging.mutinywallet.com/\", zone_id = \"2b9268714ce8d1c4431e8046d4ba55d3\" },\n    { pattern = \"nostr-staging.mutinywallet.com/event\", zone_id = \"2b9268714ce8d1c4431e8046d4ba55d3\" }\n]\nkv_namespaces = [\n  { binding = \"PUBLISHED_NOTES\", id = \"afa24a392a5a41f6b1655507dfd9b97a\", preview_id = \"0b334aece8d74c3ab90e3e99db569ce8\" },\n  { binding = \"NWC_REQUESTS\", id = \"e5bd788ddc16410bb108df0f2ae89e62\", preview_id = \"63d99f551a464ff78bbbbee7113cb658\" },\n  { binding = \"NWC_RESPONSES\", id = \"5b434d5eced84abaad1c9a44448ac71c\", preview_id = \"af27b55b58754562b4250dcd3682547b\" },\n]\n\n[[d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"blastr-db\"\ndatabase_id = \"82736a30-841d-4c24-87d8-788763dacb01\"\n\n[[env.staging.d1_databases]]\nbinding = \"DB\"\ndatabase_name = \"blastr-db-staging\"\ndatabase_id = \"ba4c227b-edf2-46a4-99fa-4b7bfa036800\"\n\n[env.staging.vars]\nWORKERS_RS_VERSION = \"0.0.18\"\nENVIRONMENT = \"staging\"\n\n[vars]\nWORKERS_RS_VERSION = \"0.0.18\"\nENVIRONMENT = \"production\"\n\n# Replace with all the queues you created, if you named them different.\n# create the queues with: `wrangler queues create {NAME}`\n# TODO make these more dynamic\n[[queues.producers]]\n queue = \"nostr-events-pub-1-b\"\n binding = \"nostr-events-pub-1-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-2-b\"\n binding = \"nostr-events-pub-2-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-3-b\"\n binding = \"nostr-events-pub-3-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-4-b\"\n binding = \"nostr-events-pub-4-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-5-b\"\n binding = \"nostr-events-pub-5-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-6-b\"\n binding = \"nostr-events-pub-6-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-7-b\"\n binding = \"nostr-events-pub-7-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-8-b\"\n binding = \"nostr-events-pub-8-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-9-b\"\n binding = \"nostr-events-pub-9-b\"\n\n[[queues.producers]]\n queue = \"nostr-events-pub-10-b\"\n binding = \"nostr-events-pub-10-b\"\n\n# consumers\n[[queues.consumers]]\n queue = \"nostr-events-pub-1-b\"\n max_batch_size = 100\n max_batch_timeout = 5 # this is the best one, run quicker\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-2-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-3-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-4-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-5-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-6-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-7-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-8-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-9-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[queues.consumers]]\n queue = \"nostr-events-pub-10-b\"\n max_batch_size = 100\n max_batch_timeout = 15\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-1-b\"\n binding = \"nostr-events-pub-1-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-2-b\"\n binding = \"nostr-events-pub-2-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-3-b\"\n binding = \"nostr-events-pub-3-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-4-b\"\n binding = \"nostr-events-pub-4-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-5-b\"\n binding = \"nostr-events-pub-5-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-6-b\"\n binding = \"nostr-events-pub-6-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-7-b\"\n binding = \"nostr-events-pub-7-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-8-b\"\n binding = \"nostr-events-pub-8-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-9-b\"\n binding = \"nostr-events-pub-9-b\"\n\n[[env.staging.queues.producers]]\n queue = \"nostr-events-pub-10-b\"\n binding = \"nostr-events-pub-10-b\"\n\n[build]\ncommand = \"cargo install -q worker-build --version 0.0.10 && worker-build --release\"\n"
  }
]