├── .env.example ├── .github ├── FUNDING.yml └── workflows │ ├── main.yml │ └── push-docker.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── c-hello-world-module ├── .gitignore ├── README.md ├── main.c ├── trinity_module.c ├── trinity_module.h └── trinity_module_component_type.o ├── java-module ├── .gitignore ├── Log.java ├── LogImpl.java ├── Sys.java ├── SysImpl.java ├── TrinityModule.java ├── TrinityModuleImpl.java ├── TrinityModuleWorld.java └── pom.xml ├── modules ├── .cargo │ └── config.toml ├── .rustfmt ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── README.md ├── convert-to-component.sh ├── horsejs │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── libcommand │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── trinity_module.rs ├── linkify │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── mastodon │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── memos │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── openai │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── pun │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── secret │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── silverbullet │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── uuid │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── wit-kv │ ├── Cargo.toml │ └── src │ │ ├── kv_world.rs │ │ └── lib.rs ├── wit-log │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── log_world.rs ├── wit-sync-request │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── sync_request_world.rs └── wit-sys │ ├── Cargo.toml │ └── src │ ├── lib.rs │ └── sys_world.rs ├── src ├── admin_table.rs ├── bin │ └── main.rs ├── lib.rs ├── room_resolver.rs ├── wasm.rs └── wasm │ └── apis │ ├── kv_store.rs │ ├── log.rs │ ├── mod.rs │ ├── sync_request.rs │ └── sys.rs └── wit ├── kv.wit ├── log.wit ├── sync-request.wit ├── sys.wit └── trinity-module.wit /.env.example: -------------------------------------------------------------------------------- 1 | # URL for the home server, without https:// prefix. 2 | HOMESERVER="matrix.example.org" 3 | # Bot account user id. 4 | BOT_USER_ID=@alice:example.org 5 | # Bot account password. 6 | BOT_PWD=hunter2 7 | # Where should the Matrix store live? 8 | MATRIX_STORE_PATH=./cache 9 | # Where should some trinity metadata be stored? 10 | REDB_PATH=./trinity.db 11 | # Who is the owner/admin user for this bot? 12 | ADMIN_USER_ID=@bob:example.org 13 | # Paths to one or multiple paths containing Trinity wasm commands, separated by commas. 14 | MODULES_PATHS=./modules/target/wasm32-unknown-unknown/release,/other/path/to/modules 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: bnjbvr 4 | liberapay: bnjbvr 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ci: 11 | env: 12 | RUST_BACKTRACE: 1 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | rust: 17 | - stable 18 | - nightly 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Install Rust 24 | uses: dtolnay/rust-toolchain@stable 25 | with: 26 | toolchain: ${{ matrix.rust }} 27 | override: true 28 | components: rustfmt 29 | 30 | - name: Load cache 31 | uses: Swatinem/rust-cache@v2 32 | 33 | - name: Format host 34 | run: | 35 | cargo fmt --all -- --check 36 | 37 | - name: Check host 38 | run: | 39 | cargo check --all --verbose 40 | 41 | ci-modules: 42 | env: 43 | RUST_BACKTRACE: 1 44 | runs-on: ubuntu-latest 45 | strategy: 46 | matrix: 47 | rust: 48 | - stable 49 | - nightly 50 | 51 | steps: 52 | - uses: actions/checkout@v3 53 | 54 | - name: Install Rust 55 | uses: dtolnay/rust-toolchain@stable 56 | with: 57 | toolchain: ${{ matrix.rust }} 58 | targets: wasm32-unknown-unknown 59 | components: rustfmt 60 | 61 | - uses: Swatinem/rust-cache@v2 62 | 63 | #- name: Format modules 64 | # run: | 65 | # cargo fmt --all --manifest-path ./modules/Cargo.toml -- --check 66 | 67 | - name: Install Protoc 68 | uses: arduino/setup-protoc@v1 69 | with: 70 | repo-token: ${{ secrets.GITHUB_TOKEN }} 71 | 72 | - name: Install wasm compile tools 73 | working-directory: ./modules/ 74 | run: | 75 | # Keep those values in sync with the Makefile! 76 | wget https://github.com/bytecodealliance/wasm-tools/releases/download/v1.229.0/wasm-tools-1.229.0-x86_64-linux.tar.gz 77 | tar pfx wasm-tools-1.229.0-x86_64-linux.tar.gz 78 | mv wasm-tools-1.229.0-x86_64-linux/wasm-tools . 79 | chmod +x wasm-tools 80 | mv wasm-tools /usr/local/bin 81 | 82 | wget https://github.com/bytecodealliance/wit-bindgen/releases/download/v0.41.0/wit-bindgen-0.41.0-x86_64-linux.tar.gz 83 | tar pfx wit-bindgen-0.41.0-x86_64-linux.tar.gz 84 | mv wit-bindgen-0.41.0-x86_64-linux/wit-bindgen . 85 | chmod +x wit-bindgen 86 | mv wit-bindgen /usr/local/bin 87 | 88 | - name: Check modules 89 | working-directory: ./modules/ 90 | run: make check 91 | -------------------------------------------------------------------------------- /.github/workflows/push-docker.yml: -------------------------------------------------------------------------------- 1 | # Source: https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images#publishing-images-to-docker-hub 2 | name: Publish Docker image 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to Docker Hub 11 | runs-on: ubuntu-latest 12 | permissions: 13 | packages: write 14 | contents: read 15 | attestations: write 16 | id-token: write 17 | steps: 18 | - name: Check out the repo 19 | uses: actions/checkout@v4 20 | 21 | - name: Log in to Docker Hub 22 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a 23 | with: 24 | username: ${{ secrets.DOCKER_USERNAME }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | 27 | - name: Extract metadata (tags, labels) for Docker 28 | id: meta 29 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 30 | with: 31 | images: bnjbvr/trinity 32 | 33 | - name: Build and push Docker image 34 | id: push 35 | uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 36 | with: 37 | context: . 38 | file: ./Dockerfile 39 | push: true 40 | tags: ${{ steps.meta.outputs.tags }} 41 | labels: ${{ steps.meta.outputs.labels }} 42 | 43 | - name: Generate artifact attestation 44 | uses: actions/attest-build-provenance@v2 45 | with: 46 | subject-name: docker.io/bnjbvr/trinity 47 | subject-digest: ${{ steps.push.outputs.digest }} 48 | push-to-registry: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .env 3 | trinity.db 4 | cache/ 5 | data/ 6 | modules/**/src/bindings.rs 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trinity" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "trinity" 8 | path = "src/lib.rs" 9 | 10 | [[bin]] 11 | name = "trinity" 12 | path = "src/bin/main.rs" 13 | 14 | [dependencies] 15 | anyhow = "1.0.66" 16 | dotenvy = "0.15.6" 17 | futures = "0.3.25" 18 | matrix-sdk = "0.6.2" 19 | notify = "5.0.0" 20 | rand = "0.8.5" 21 | redb = "0.9.0" 22 | reqwest = { version = "0.11.12", features = ["json", "blocking"] } 23 | signal-hook = "0.3.15" 24 | signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } 25 | serde = { version = "1.0.152", features = ["derive"] } 26 | tokio = { version = "1.38.2", features = ["rt-multi-thread", "macros"] } 27 | toml = "0.5.10" 28 | tracing = "0.1.37" 29 | tracing-subscriber = "0.3.16" 30 | wasmtime = { version = "32.0.0", features = ["component-model"] } 31 | directories = "5.0.1" 32 | rayon = "1.10.0" 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build image. 2 | 3 | FROM rust:1.84 AS builder 4 | LABEL maintainer="Benjamin Bouvier " 5 | 6 | RUN mkdir -p /build/modules 7 | 8 | COPY ./Cargo.* /build/ 9 | COPY ./src /build/src 10 | COPY ./wit /build/wit 11 | COPY ./modules /build/modules/ 12 | 13 | # Compile the host. 14 | WORKDIR /build/src 15 | RUN cargo build --release 16 | 17 | # Set up protoc. 18 | ENV PROTOC_ZIP=protoc-23.0-linux-x86_64.zip 19 | 20 | RUN curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v23.0/$PROTOC_ZIP && \ 21 | unzip -o $PROTOC_ZIP -d /usr/local bin/protoc && \ 22 | unzip -o $PROTOC_ZIP -d /usr/local 'include/*' && \ 23 | chmod +x /usr/local/bin/protoc && \ 24 | rm -f $PROTOC_ZIP 25 | 26 | ENV PROTOC=/usr/local/bin/protoc 27 | 28 | WORKDIR /build/modules 29 | RUN make install-tools && \ 30 | rustup component add rustfmt && \ 31 | rustup target add wasm32-unknown-unknown 32 | RUN make release 33 | 34 | # Actual image. 35 | FROM debian:bookworm-slim 36 | 37 | RUN apt-get update && \ 38 | apt-get install -y ca-certificates && \ 39 | rm -rf /var/lib/apt/lists/* && \ 40 | update-ca-certificates && \ 41 | mkdir -p /opt/trinity/data && \ 42 | mkdir -p /opt/trinity/modules/target/wasm32-unknown-unknown/release 43 | 44 | COPY --from=builder /build/target/release/trinity /opt/trinity/trinity 45 | COPY --from=builder \ 46 | /build/modules/target/wasm32-unknown-unknown/release/*.wasm \ 47 | /opt/trinity/modules/target/wasm32-unknown-unknown/release 48 | 49 | ENV MATRIX_STORE_PATH /opt/trinity/data/cache 50 | ENV REDB_PATH /opt/trinity/data/db 51 | 52 | VOLUME /opt/trinity/data 53 | 54 | WORKDIR /opt/trinity 55 | CMD /opt/trinity/trinity 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

trinity

3 | 4 |

5 | Matrix bots in Rust and WebAssembly 6 |

7 | 8 |

9 | build status 10 | matrix chat 11 | supported rustc stable 12 |

13 |
14 | 15 | ## TL;DR 16 | 17 | Trinity is an experimental bot framework written in Rust and using matrix-rust-sdk, as well as 18 | commands / modules compiled to WebAssembly, with convenient developer features like modules 19 | hot-reload. 20 | 21 | ## What is this? 22 | 23 | This started as a fun weekend project where I've written a new generic Matrix bot framework. It is 24 | written in Rust from scratch using the fantastic 25 | [matrix-rust-sdk](https://github.com/matrix-org/matrix-rust-sdk) crate. 26 | 27 | Bot commands can be implemented as WebAssembly components, using 28 | [Wasmtime](https://github.com/bytecodealliance/wasmtime) as the WebAssembly virtual machine, and 29 | [wit-bindgen](https://github.com/bytecodealliance/wit-bindgen) for conveniently implementing 30 | interfaces between the host and wasm modules. 31 | 32 | See for instance the [`uuid`](https://github.com/bnjbvr/trinity/blob/main/modules/uuid/src/lib.rs) 33 | and [`horsejs`](https://github.com/bnjbvr/trinity/blob/main/modules/horsejs/src/lib.rs) modules. 34 | 35 | Make sure to install the required tools (as of this writing, `wit-bindgen` and `wasm-tools`) 36 | to be able to build wasm components. We're using a pinned revision of this that can automatically 37 | be installed with `./modules/install-tools.sh` at the moment; we hope to lift that 38 | limitation in the future. 39 | 40 | Modules can be hot-reloaded, making it trivial to deploy new modules, or replace existing modules 41 | already running on a server. It is also nice during development iterations on modules. Basically 42 | one can do the following to see changes in close to real-time: 43 | 44 | - run trinity with `cargo run` 45 | - `cd modules/ && cargo watch -x "component build --target=wasm32-unknown-unknown --release"` in another terminal 46 | 47 | The overall generic design is inspired from my previous bot, 48 | [botzilla](https://github.com/bnjbvr/botzilla), that was written in JavaScript and was very 49 | specialized for Mozilla needs. 50 | 51 | ## Want / "roadmap" 52 | 53 | At this point I expect this to be more of a weekend project, so I won't commit to any of those, but 54 | here's my ideas of things to implement in the future. If you feel like implementing some of these 55 | ideas, please go ahead :) 56 | 57 | ### Core features 58 | 59 | - fetch and cache user names 60 | - make it possible to answer privately / to the full room / as a reply to the original message / as 61 | a thread reply. 62 | - add ability to set emojis on specific messages (? this was useful for the admin module in botzilla) 63 | - moonshot: JS host so one can test the chat modules on a Web browser, without requiring a matrix 64 | account 65 | - marsshot: existing modules built from CI and pushed to a simple Web app on github-pages that 66 | allows selecting an individual module and trying it. 67 | - seek other `TODO` in code :p 68 | 69 | ### Modules 70 | 71 | - post on twitter. Example: `!tweet Inflammatory take that will receive millions of likes and quote-tweets` 72 | - same requirements as mastodon, likely 73 | - gitlab auto-link to issues/merge requests: e.g. if someone types `!123`, post a link to 74 | `https://{GITLAB_REPO_URL}/-/issues/123`. 75 | - would require the room to be configured with a gitlab repository 76 | - same for github would be sweet 77 | - ask what's the weather in some city, is it going to rain in the next hour, etc. 78 | - YOUR BILLION DOLLARS IDEA HERE 79 | 80 | ## Deploying 81 | 82 | ### Docker 83 | 84 | If you want, you can use the image published on Docker 85 | ([bnjbvr/trinity](https://hub.docker.com/repository/docker/bnjbvr/trinity)) -- it might be lagging 86 | behind by a few commits -- or build the Docker image yourself: 87 | 88 | ``` 89 | docker build -t bnjbvr/trinity . 90 | ``` 91 | 92 | Then start it with the right environment variables (see also `.env.example`): 93 | 94 | ``` 95 | docker run -e HOMESERVER="matrix.example.com" \ 96 | -e BOT_USER_ID="@trinity:example.com" \ 97 | -e BOT_PWD="hunter2" \ 98 | -e ADMIN_USER_ID="@admin:example.com" \ 99 | -v /host/path/to/data/directory:/opt/trinity/data \ 100 | -ti bnjbvr/trinity 101 | ``` 102 | 103 | Data is saved in the `/opt/trinity/data` directory, and it is recommended to make it a volume so as 104 | to be able to decrypt messages over multiple sessions and so on. 105 | 106 | ### Custom Modules 107 | 108 | If you want, you can specify a custom modules directory using the `MODULES_PATHS` environment 109 | variable and adding another data volume for it. This can be useful for hacking modules only without 110 | having to compile the host runtime. Here's an example using Docker: 111 | 112 | ``` 113 | docker run -e HOMESERVER="matrix.example.com" \ 114 | -e BOT_USER_ID="@trinity:example.com" \ 115 | -e BOT_PWD="hunter2" \ 116 | -e ADMIN_USER_ID="@admin:example.com" \ 117 | -e MODULES_PATH="/wasm-modules" \ 118 | -v /host/path/to/data/directory:/opt/trinity/data \ 119 | -v /host/path/to/modules:/wasm-modules \ 120 | -ti bnjbvr/trinity 121 | ``` 122 | 123 | ### Configuration 124 | 125 | Trinity can be configured via config file. The config file can be passed in from the command line: 126 | 127 | ```bash 128 | cargo run -- config.toml 129 | ``` 130 | 131 | Or it can be placed in `$XDG_CONFIG_HOME`, typically `~/.config/trinity/config.toml` on XDG 132 | compliant systems. Configuration lives in the document root, for example: 133 | 134 | ```toml 135 | home_server = "matrix.example.com" 136 | user_id = "@trinity:example.com" 137 | password = "hunter2" 138 | matrix_store_path = "/path/to/store" 139 | redb_path = "/path/to/redb" 140 | admin_user_id = "@admin:example.com" 141 | modules_path = ["/wasm-modules"] 142 | ``` 143 | 144 | ### Module Configuration 145 | 146 | It's also possible to pass arbitrary configuration down to specific modules in the config 147 | file. For example: 148 | 149 | ```toml 150 | [modules_config.pun] 151 | format = "image" 152 | ``` 153 | 154 | This passes the object `{"format": "image"}` to the `pun` module's `init` function. It's 155 | up to specific modules to handle this configuration. 156 | 157 | ## Is it any good? 158 | 159 | [Yes](https://news.ycombinator.com/item?id=3067434). 160 | 161 | ## Contributing 162 | 163 | [![Contributor Covenant](https://img.shields.io/badge/contributor%20covenant-v1.4-ff69b4.svg)](https://www.contributor-covenant.org/version/1/4/code-of-conduct/) 164 | 165 | We welcome community contributions to this project. 166 | 167 | ## Why the name? 168 | 169 | This is a *Matrix* bot, coded in Rust and WebAssembly, forming a holy trinity of technologies I 170 | love. And, Trinity is also a bad-ass character from the Matrix movie franchise. 171 | 172 | ## License 173 | 174 | [LGPLv2 license](LICENSE.md). 175 | -------------------------------------------------------------------------------- /c-hello-world-module/.gitignore: -------------------------------------------------------------------------------- 1 | out.wasm 2 | -------------------------------------------------------------------------------- /c-hello-world-module/README.md: -------------------------------------------------------------------------------- 1 | # C module 2 | 3 | These are rough notes of how I did make it work: 4 | 5 | - Generated glue with: 6 | 7 | ``` 8 | wit-bindgen guest c -i ./wit/sys.wit -i ./wit/log.wit -d ./wit/trinity-module.wit --out-dir /tmp/c 9 | ``` 10 | 11 | - wrote the code in `main.c`. Probably full of memory leaks and footguns there. I can't C clearly anymore. 12 | - Compiled with: 13 | 14 | ``` 15 | ~/code/wasi-sdk/wasi-sdk-16.0/bin/clang \ 16 | --sysroot ~/code/wasi-sdk/wasi-sdk-16.0/share/wasi-sysroot 17 | ./main.c 18 | ./trinity_module.c 19 | ./trinity_module_component_type.o 20 | -Wall -Wextra -Wno-unused-parameter 21 | -mexec-model=reactor 22 | -g 23 | -o out.wasm 24 | ``` 25 | 26 | - converted to a wasm component with: 27 | 28 | ``` 29 | wit-component ./out.wasm 30 | ``` 31 | 32 | - moved to the watched directory of trinity: 33 | 34 | ``` 35 | cp out.wasm ../modules/target/wasm32-unknown-unknown/release 36 | ``` 37 | -------------------------------------------------------------------------------- /c-hello-world-module/main.c: -------------------------------------------------------------------------------- 1 | #include "trinity_module.h" 2 | 3 | void trinity_module_init(void) { 4 | trinity_module_string_t str = {.ptr = "hello", .len = strlen("hello")}; 5 | log_debug(&str); 6 | } 7 | 8 | void trinity_module_help(trinity_module_option_string_t *topic, 9 | trinity_module_string_t *ret) { 10 | trinity_module_string_set(ret, 11 | "a simple module showing how trinity works in C"); 12 | } 13 | 14 | void trinity_module_admin(trinity_module_string_t *cmd, 15 | trinity_module_string_t *author_id, 16 | trinity_module_list_message_t *ret) { 17 | ret->len = 0; 18 | } 19 | 20 | void trinity_module_on_msg(trinity_module_string_t *content, 21 | trinity_module_string_t *author_id, 22 | trinity_module_string_t *author_name, 23 | trinity_module_string_t *room, 24 | trinity_module_list_message_t *ret) { 25 | trinity_module_message_t *msg = malloc(sizeof(trinity_module_message_t)); 26 | ret->ptr = msg; 27 | ret->len = 1; 28 | 29 | msg->to.ptr = author_id->ptr; 30 | msg->to.len = author_id->len; 31 | 32 | char *ptr = 33 | malloc(sizeof(char) * (strlen(author_id->ptr) + strlen("Hello, !"))); 34 | 35 | // ogod this is dirty. what it takes to not use wasi 🙈 36 | strcpy(ptr, "Hello, "); 37 | strcpy(ptr + strlen("Hello, "), author_id->ptr); 38 | strcpy(ptr + strlen("Hello, ") + strlen(author_id->ptr), "!"); 39 | 40 | msg->content.ptr = ptr; 41 | msg->content.len = strlen(ptr); 42 | } 43 | -------------------------------------------------------------------------------- /c-hello-world-module/trinity_module.c: -------------------------------------------------------------------------------- 1 | #include "trinity_module.h" 2 | 3 | __attribute__((weak, export_name("cabi_post_help"))) 4 | void __wasm_export_trinity_module_help_post_return(int32_t arg0) { 5 | if ((*((int32_t*) (arg0 + 4))) > 0) { 6 | free((void*) (*((int32_t*) (arg0 + 0)))); 7 | } 8 | } 9 | __attribute__((weak, export_name("cabi_post_admin"))) 10 | void __wasm_export_trinity_module_admin_post_return(int32_t arg0) { 11 | int32_t ptr = *((int32_t*) (arg0 + 0)); 12 | int32_t len = *((int32_t*) (arg0 + 4)); 13 | for (int32_t i = 0; i < len; i++) { 14 | int32_t base = ptr + i * 16; 15 | (void) base; 16 | if ((*((int32_t*) (base + 4))) > 0) { 17 | free((void*) (*((int32_t*) (base + 0)))); 18 | } 19 | if ((*((int32_t*) (base + 12))) > 0) { 20 | free((void*) (*((int32_t*) (base + 8)))); 21 | } 22 | } 23 | if (len > 0) { 24 | free((void*) (ptr)); 25 | } 26 | } 27 | __attribute__((weak, export_name("cabi_post_on-msg"))) 28 | void __wasm_export_trinity_module_on_msg_post_return(int32_t arg0) { 29 | int32_t ptr = *((int32_t*) (arg0 + 0)); 30 | int32_t len = *((int32_t*) (arg0 + 4)); 31 | for (int32_t i = 0; i < len; i++) { 32 | int32_t base = ptr + i * 16; 33 | (void) base; 34 | if ((*((int32_t*) (base + 4))) > 0) { 35 | free((void*) (*((int32_t*) (base + 0)))); 36 | } 37 | if ((*((int32_t*) (base + 12))) > 0) { 38 | free((void*) (*((int32_t*) (base + 8)))); 39 | } 40 | } 41 | if (len > 0) { 42 | free((void*) (ptr)); 43 | } 44 | } 45 | 46 | __attribute__((weak, export_name("cabi_realloc"))) 47 | void *cabi_realloc(void *ptr, size_t orig_size, size_t org_align, size_t new_size) { 48 | void *ret = realloc(ptr, new_size); 49 | if (!ret) abort(); 50 | return ret; 51 | } 52 | 53 | // Helper Functions 54 | 55 | void trinity_module_message_free(trinity_module_message_t *ptr) { 56 | trinity_module_string_free(&ptr->content); 57 | trinity_module_string_free(&ptr->to); 58 | } 59 | 60 | void trinity_module_option_string_free(trinity_module_option_string_t *ptr) { 61 | if (ptr->is_some) { 62 | trinity_module_string_free(&ptr->val); 63 | } 64 | } 65 | 66 | void trinity_module_list_message_free(trinity_module_list_message_t *ptr) { 67 | for (size_t i = 0; i < ptr->len; i++) { 68 | trinity_module_message_free(&ptr->ptr[i]); 69 | } 70 | if (ptr->len > 0) { 71 | free(ptr->ptr); 72 | } 73 | } 74 | 75 | void trinity_module_string_set(trinity_module_string_t *ret, const char*s) { 76 | ret->ptr = (char*) s; 77 | ret->len = strlen(s); 78 | } 79 | 80 | void trinity_module_string_dup(trinity_module_string_t *ret, const char*s) { 81 | ret->len = strlen(s); 82 | ret->ptr = cabi_realloc(NULL, 0, 1, ret->len * 1); 83 | memcpy(ret->ptr, s, ret->len * 1); 84 | } 85 | 86 | void trinity_module_string_free(trinity_module_string_t *ret) { 87 | if (ret->len > 0) { 88 | free(ret->ptr); 89 | } 90 | ret->ptr = NULL; 91 | ret->len = 0; 92 | } 93 | 94 | // Component Adapters 95 | 96 | __attribute__((aligned(4))) 97 | static uint8_t RET_AREA[8]; 98 | 99 | uint64_t sys_rand_u64(void) { 100 | int64_t ret = __wasm_import_sys_rand_u64(); 101 | return (uint64_t) (ret); 102 | } 103 | 104 | void log_trace(trinity_module_string_t *s) { 105 | __wasm_import_log_trace((int32_t) (*s).ptr, (int32_t) (*s).len); 106 | } 107 | 108 | void log_debug(trinity_module_string_t *s) { 109 | __wasm_import_log_debug((int32_t) (*s).ptr, (int32_t) (*s).len); 110 | } 111 | 112 | void log_info(trinity_module_string_t *s) { 113 | __wasm_import_log_info((int32_t) (*s).ptr, (int32_t) (*s).len); 114 | } 115 | 116 | void log_warn(trinity_module_string_t *s) { 117 | __wasm_import_log_warn((int32_t) (*s).ptr, (int32_t) (*s).len); 118 | } 119 | 120 | void log_error(trinity_module_string_t *s) { 121 | __wasm_import_log_error((int32_t) (*s).ptr, (int32_t) (*s).len); 122 | } 123 | 124 | __attribute__((export_name("init"))) 125 | void __wasm_export_trinity_module_init(void) { 126 | trinity_module_init(); 127 | } 128 | 129 | __attribute__((export_name("help"))) 130 | int32_t __wasm_export_trinity_module_help(int32_t arg, int32_t arg0, int32_t arg1) { 131 | trinity_module_option_string_t option; 132 | switch (arg) { 133 | case 0: { 134 | option.is_some = false; 135 | break; 136 | } 137 | case 1: { 138 | option.is_some = true; 139 | option.val = (trinity_module_string_t) { (char*)(arg0), (size_t)(arg1) }; 140 | break; 141 | } 142 | } 143 | trinity_module_option_string_t arg2 = option; 144 | trinity_module_string_t ret; 145 | trinity_module_help(&arg2, &ret); 146 | int32_t ptr = (int32_t) &RET_AREA; 147 | *((int32_t*)(ptr + 4)) = (int32_t) (ret).len; 148 | *((int32_t*)(ptr + 0)) = (int32_t) (ret).ptr; 149 | return ptr; 150 | } 151 | 152 | __attribute__((export_name("admin"))) 153 | int32_t __wasm_export_trinity_module_admin(int32_t arg, int32_t arg0, int32_t arg1, int32_t arg2) { 154 | trinity_module_string_t arg3 = (trinity_module_string_t) { (char*)(arg), (size_t)(arg0) }; 155 | trinity_module_string_t arg4 = (trinity_module_string_t) { (char*)(arg1), (size_t)(arg2) }; 156 | trinity_module_list_message_t ret; 157 | trinity_module_admin(&arg3, &arg4, &ret); 158 | int32_t ptr = (int32_t) &RET_AREA; 159 | *((int32_t*)(ptr + 4)) = (int32_t) (ret).len; 160 | *((int32_t*)(ptr + 0)) = (int32_t) (ret).ptr; 161 | return ptr; 162 | } 163 | 164 | __attribute__((export_name("on-msg"))) 165 | int32_t __wasm_export_trinity_module_on_msg(int32_t arg, int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3, int32_t arg4, int32_t arg5, int32_t arg6) { 166 | trinity_module_string_t arg7 = (trinity_module_string_t) { (char*)(arg), (size_t)(arg0) }; 167 | trinity_module_string_t arg8 = (trinity_module_string_t) { (char*)(arg1), (size_t)(arg2) }; 168 | trinity_module_string_t arg9 = (trinity_module_string_t) { (char*)(arg3), (size_t)(arg4) }; 169 | trinity_module_string_t arg10 = (trinity_module_string_t) { (char*)(arg5), (size_t)(arg6) }; 170 | trinity_module_list_message_t ret; 171 | trinity_module_on_msg(&arg7, &arg8, &arg9, &arg10, &ret); 172 | int32_t ptr = (int32_t) &RET_AREA; 173 | *((int32_t*)(ptr + 4)) = (int32_t) (ret).len; 174 | *((int32_t*)(ptr + 0)) = (int32_t) (ret).ptr; 175 | return ptr; 176 | } 177 | 178 | extern void __component_type_object_force_link_trinity_module(void); 179 | void __component_type_object_force_link_trinity_module_public_use_in_this_compilation_unit(void) { 180 | __component_type_object_force_link_trinity_module(); 181 | } 182 | -------------------------------------------------------------------------------- /c-hello-world-module/trinity_module.h: -------------------------------------------------------------------------------- 1 | #ifndef __BINDINGS_TRINITY_MODULE_H 2 | #define __BINDINGS_TRINITY_MODULE_H 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | //#include 8 | #include 9 | #include 10 | #include 11 | 12 | typedef struct { 13 | char*ptr; 14 | size_t len; 15 | } trinity_module_string_t; 16 | 17 | typedef struct { 18 | trinity_module_string_t content; 19 | trinity_module_string_t to; 20 | } trinity_module_message_t; 21 | 22 | typedef struct { 23 | bool is_some; 24 | trinity_module_string_t val; 25 | } trinity_module_option_string_t; 26 | 27 | typedef struct { 28 | trinity_module_message_t *ptr; 29 | size_t len; 30 | } trinity_module_list_message_t; 31 | 32 | // Imported Functions from `sys` 33 | 34 | __attribute__((import_module("sys"), import_name("rand-u64"))) 35 | int64_t __wasm_import_sys_rand_u64(void); 36 | uint64_t sys_rand_u64(void); 37 | 38 | // Imported Functions from `log` 39 | 40 | __attribute__((import_module("log"), import_name("trace"))) 41 | void __wasm_import_log_trace(int32_t, int32_t); 42 | void log_trace(trinity_module_string_t *s); 43 | 44 | __attribute__((import_module("log"), import_name("debug"))) 45 | void __wasm_import_log_debug(int32_t, int32_t); 46 | void log_debug(trinity_module_string_t *s); 47 | 48 | __attribute__((import_module("log"), import_name("info"))) 49 | void __wasm_import_log_info(int32_t, int32_t); 50 | void log_info(trinity_module_string_t *s); 51 | 52 | __attribute__((import_module("log"), import_name("warn"))) 53 | void __wasm_import_log_warn(int32_t, int32_t); 54 | void log_warn(trinity_module_string_t *s); 55 | 56 | __attribute__((import_module("log"), import_name("error"))) 57 | void __wasm_import_log_error(int32_t, int32_t); 58 | void log_error(trinity_module_string_t *s); 59 | 60 | // Exported Functions from `trinity-module` 61 | void trinity_module_init(void); 62 | void trinity_module_help(trinity_module_option_string_t *topic, trinity_module_string_t *ret); 63 | void trinity_module_admin(trinity_module_string_t *cmd, trinity_module_string_t *author_id, trinity_module_list_message_t *ret); 64 | void trinity_module_on_msg(trinity_module_string_t *content, trinity_module_string_t *author_id, trinity_module_string_t *author_name, trinity_module_string_t *room, trinity_module_list_message_t *ret); 65 | 66 | // Helper Functions 67 | 68 | void trinity_module_message_free(trinity_module_message_t *ptr); 69 | void trinity_module_option_string_free(trinity_module_option_string_t *ptr); 70 | void trinity_module_list_message_free(trinity_module_list_message_t *ptr); 71 | void trinity_module_string_set(trinity_module_string_t *ret, const char*s); 72 | void trinity_module_string_dup(trinity_module_string_t *ret, const char*s); 73 | void trinity_module_string_free(trinity_module_string_t *ret); 74 | 75 | #ifdef __cplusplus 76 | } 77 | #endif 78 | #endif 79 | -------------------------------------------------------------------------------- /c-hello-world-module/trinity_module_component_type.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnjbvr/trinity/a5ff2b8b2158c7b7988c2dca5171ffcf60283b01/c-hello-world-module/trinity_module_component_type.o -------------------------------------------------------------------------------- /java-module/.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | -------------------------------------------------------------------------------- /java-module/Log.java: -------------------------------------------------------------------------------- 1 | package wit_trinity_module; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.ArrayList; 5 | 6 | import org.teavm.interop.Memory; 7 | import org.teavm.interop.Address; 8 | import org.teavm.interop.Import; 9 | import org.teavm.interop.Export; 10 | 11 | public final class Log { 12 | private Log() {} 13 | 14 | @Import(name = "trace", module = "log") 15 | private static native void wasmImportTrace(int p0, int p1); 16 | 17 | public static void trace(String s) { 18 | byte[] bytes = (s).getBytes(StandardCharsets.UTF_8); 19 | wasmImportTrace(Address.ofData(bytes).toInt(), bytes.length); 20 | 21 | } 22 | @Import(name = "debug", module = "log") 23 | private static native void wasmImportDebug(int p0, int p1); 24 | 25 | public static void debug(String s) { 26 | byte[] bytes = (s).getBytes(StandardCharsets.UTF_8); 27 | wasmImportDebug(Address.ofData(bytes).toInt(), bytes.length); 28 | 29 | } 30 | @Import(name = "info", module = "log") 31 | private static native void wasmImportInfo(int p0, int p1); 32 | 33 | public static void info(String s) { 34 | byte[] bytes = (s).getBytes(StandardCharsets.UTF_8); 35 | wasmImportInfo(Address.ofData(bytes).toInt(), bytes.length); 36 | 37 | } 38 | @Import(name = "warn", module = "log") 39 | private static native void wasmImportWarn(int p0, int p1); 40 | 41 | public static void warn(String s) { 42 | byte[] bytes = (s).getBytes(StandardCharsets.UTF_8); 43 | wasmImportWarn(Address.ofData(bytes).toInt(), bytes.length); 44 | 45 | } 46 | @Import(name = "error", module = "log") 47 | private static native void wasmImportError(int p0, int p1); 48 | 49 | public static void error(String s) { 50 | byte[] bytes = (s).getBytes(StandardCharsets.UTF_8); 51 | wasmImportError(Address.ofData(bytes).toInt(), bytes.length); 52 | 53 | } 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /java-module/LogImpl.java: -------------------------------------------------------------------------------- 1 | package wit_trinity_module; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.ArrayList; 5 | 6 | import org.teavm.interop.Memory; 7 | import org.teavm.interop.Address; 8 | import org.teavm.interop.Import; 9 | import org.teavm.interop.Export; 10 | 11 | public class LogImpl { 12 | 13 | } 14 | 15 | -------------------------------------------------------------------------------- /java-module/Sys.java: -------------------------------------------------------------------------------- 1 | package wit_trinity_module; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.ArrayList; 5 | 6 | import org.teavm.interop.Memory; 7 | import org.teavm.interop.Address; 8 | import org.teavm.interop.Import; 9 | import org.teavm.interop.Export; 10 | 11 | public final class Sys { 12 | private Sys() {} 13 | 14 | @Import(name = "rand-u64", module = "sys") 15 | private static native long wasmImportRandU64(); 16 | 17 | public static long randU64() { 18 | long result = wasmImportRandU64(); 19 | return result; 20 | 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /java-module/SysImpl.java: -------------------------------------------------------------------------------- 1 | package wit_trinity_module; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.ArrayList; 5 | 6 | import org.teavm.interop.Memory; 7 | import org.teavm.interop.Address; 8 | import org.teavm.interop.Import; 9 | import org.teavm.interop.Export; 10 | 11 | public class SysImpl { 12 | 13 | } 14 | 15 | -------------------------------------------------------------------------------- /java-module/TrinityModule.java: -------------------------------------------------------------------------------- 1 | package wit_trinity_module; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.ArrayList; 5 | 6 | import org.teavm.interop.Memory; 7 | import org.teavm.interop.Address; 8 | import org.teavm.interop.Import; 9 | import org.teavm.interop.Export; 10 | 11 | public final class TrinityModule { 12 | private TrinityModule() {} 13 | 14 | public static final class Message { 15 | public final String content; 16 | public final String to; 17 | 18 | public Message(String content, String to) { 19 | this.content = content; 20 | this.to = to; 21 | } 22 | } 23 | 24 | @Export(name = "init") 25 | private static void wasmExportInit() { 26 | 27 | TrinityModuleImpl.init(); 28 | 29 | } 30 | 31 | @Export(name = "help") 32 | private static int wasmExportHelp(int p0, int p1, int p2) { 33 | 34 | String lifted; 35 | 36 | switch (p0) { 37 | case 0: { 38 | lifted = null; 39 | break; 40 | } 41 | 42 | case 1: { 43 | 44 | byte[] bytes = new byte[p2]; 45 | Memory.getBytes(Address.fromInt(p1), bytes, 0, p2); 46 | 47 | lifted = new String(bytes, StandardCharsets.UTF_8); 48 | break; 49 | } 50 | 51 | default: throw new AssertionError("invalid discriminant: " + (p0)); 52 | } 53 | 54 | String result = TrinityModuleImpl.help(lifted); 55 | 56 | byte[] bytes2 = (result).getBytes(StandardCharsets.UTF_8); 57 | 58 | Address address = Memory.malloc(bytes2.length, 1); 59 | Memory.putBytes(address, bytes2, 0, bytes2.length); 60 | Address.fromInt((TrinityModuleWorld.RETURN_AREA) + 4).putInt(bytes2.length); 61 | Address.fromInt((TrinityModuleWorld.RETURN_AREA) + 0).putInt(address.toInt()); 62 | return TrinityModuleWorld.RETURN_AREA; 63 | 64 | } 65 | 66 | @Export(name = "cabi_post_help") 67 | private static void wasmExportHelpPostReturn(int p0) { 68 | Memory.free(Address.fromInt(Address.fromInt((p0) + 0).getInt()), Address.fromInt((p0) + 4).getInt(), 1); 69 | 70 | } 71 | 72 | @Export(name = "admin") 73 | private static int wasmExportAdmin(int p0, int p1, int p2, int p3) { 74 | 75 | byte[] bytes = new byte[p1]; 76 | Memory.getBytes(Address.fromInt(p0), bytes, 0, p1); 77 | 78 | byte[] bytes0 = new byte[p3]; 79 | Memory.getBytes(Address.fromInt(p2), bytes0, 0, p3); 80 | 81 | ArrayList result = TrinityModuleImpl.admin(new String(bytes, StandardCharsets.UTF_8), new String(bytes0, StandardCharsets.UTF_8)); 82 | 83 | int address4 = Memory.malloc((result).size() * 16, 4).toInt(); 84 | for (int index = 0; index < (result).size(); ++index) { 85 | Message element = (result).get(index); 86 | int base = address4 + (index * 16); 87 | byte[] bytes1 = ((element).content).getBytes(StandardCharsets.UTF_8); 88 | 89 | Address address = Memory.malloc(bytes1.length, 1); 90 | Memory.putBytes(address, bytes1, 0, bytes1.length); 91 | Address.fromInt((base) + 4).putInt(bytes1.length); 92 | Address.fromInt((base) + 0).putInt(address.toInt()); 93 | byte[] bytes2 = ((element).to).getBytes(StandardCharsets.UTF_8); 94 | 95 | Address address3 = Memory.malloc(bytes2.length, 1); 96 | Memory.putBytes(address3, bytes2, 0, bytes2.length); 97 | Address.fromInt((base) + 12).putInt(bytes2.length); 98 | Address.fromInt((base) + 8).putInt(address3.toInt()); 99 | 100 | } 101 | Address.fromInt((TrinityModuleWorld.RETURN_AREA) + 4).putInt((result).size()); 102 | Address.fromInt((TrinityModuleWorld.RETURN_AREA) + 0).putInt(address4); 103 | return TrinityModuleWorld.RETURN_AREA; 104 | 105 | } 106 | 107 | @Export(name = "cabi_post_admin") 108 | private static void wasmExportAdminPostReturn(int p0) { 109 | 110 | for (int index = 0; index < (Address.fromInt((p0) + 4).getInt()); ++index) { 111 | int base = (Address.fromInt((p0) + 0).getInt()) + (index * 16); 112 | Memory.free(Address.fromInt(Address.fromInt((base) + 0).getInt()), Address.fromInt((base) + 4).getInt(), 1); 113 | Memory.free(Address.fromInt(Address.fromInt((base) + 8).getInt()), Address.fromInt((base) + 12).getInt(), 1); 114 | 115 | } 116 | Memory.free(Address.fromInt(Address.fromInt((p0) + 0).getInt()), (Address.fromInt((p0) + 4).getInt()) * 16, 4); 117 | 118 | } 119 | 120 | @Export(name = "on-msg") 121 | private static int wasmExportOnMsg(int p0, int p1, int p2, int p3, int p4, int p5, int p6, int p7) { 122 | 123 | byte[] bytes = new byte[p1]; 124 | Memory.getBytes(Address.fromInt(p0), bytes, 0, p1); 125 | 126 | byte[] bytes0 = new byte[p3]; 127 | Memory.getBytes(Address.fromInt(p2), bytes0, 0, p3); 128 | 129 | byte[] bytes1 = new byte[p5]; 130 | Memory.getBytes(Address.fromInt(p4), bytes1, 0, p5); 131 | 132 | byte[] bytes2 = new byte[p7]; 133 | Memory.getBytes(Address.fromInt(p6), bytes2, 0, p7); 134 | 135 | ArrayList result = TrinityModuleImpl.onMsg(new String(bytes, StandardCharsets.UTF_8), new String(bytes0, StandardCharsets.UTF_8), new String(bytes1, StandardCharsets.UTF_8), new String(bytes2, StandardCharsets.UTF_8)); 136 | 137 | int address6 = Memory.malloc((result).size() * 16, 4).toInt(); 138 | for (int index = 0; index < (result).size(); ++index) { 139 | Message element = (result).get(index); 140 | int base = address6 + (index * 16); 141 | byte[] bytes3 = ((element).content).getBytes(StandardCharsets.UTF_8); 142 | 143 | Address address = Memory.malloc(bytes3.length, 1); 144 | Memory.putBytes(address, bytes3, 0, bytes3.length); 145 | Address.fromInt((base) + 4).putInt(bytes3.length); 146 | Address.fromInt((base) + 0).putInt(address.toInt()); 147 | byte[] bytes4 = ((element).to).getBytes(StandardCharsets.UTF_8); 148 | 149 | Address address5 = Memory.malloc(bytes4.length, 1); 150 | Memory.putBytes(address5, bytes4, 0, bytes4.length); 151 | Address.fromInt((base) + 12).putInt(bytes4.length); 152 | Address.fromInt((base) + 8).putInt(address5.toInt()); 153 | 154 | } 155 | Address.fromInt((TrinityModuleWorld.RETURN_AREA) + 4).putInt((result).size()); 156 | Address.fromInt((TrinityModuleWorld.RETURN_AREA) + 0).putInt(address6); 157 | return TrinityModuleWorld.RETURN_AREA; 158 | 159 | } 160 | 161 | @Export(name = "cabi_post_on-msg") 162 | private static void wasmExportOnMsgPostReturn(int p0) { 163 | 164 | for (int index = 0; index < (Address.fromInt((p0) + 4).getInt()); ++index) { 165 | int base = (Address.fromInt((p0) + 0).getInt()) + (index * 16); 166 | Memory.free(Address.fromInt(Address.fromInt((base) + 0).getInt()), Address.fromInt((base) + 4).getInt(), 1); 167 | Memory.free(Address.fromInt(Address.fromInt((base) + 8).getInt()), Address.fromInt((base) + 12).getInt(), 1); 168 | 169 | } 170 | Memory.free(Address.fromInt(Address.fromInt((p0) + 0).getInt()), (Address.fromInt((p0) + 4).getInt()) * 16, 4); 171 | 172 | } 173 | 174 | } 175 | 176 | -------------------------------------------------------------------------------- /java-module/TrinityModuleImpl.java: -------------------------------------------------------------------------------- 1 | package wit_trinity_module; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class TrinityModuleImpl { 6 | public static void init() { 7 | Log.trace("Hello, world!"); 8 | } 9 | 10 | public static String help(String topic) { 11 | return "Requested help for topic " + topic; 12 | } 13 | 14 | public static ArrayList admin(String cmd, String authorId) { 15 | return new ArrayList(); 16 | } 17 | 18 | public static ArrayList onMsg(String content, String authorId, String authorName, String room) { 19 | TrinityModule.Message msg = new TrinityModule.Message("Hello, " + authorId + "!", authorId); 20 | ArrayList list = new ArrayList(); 21 | list.push(msg); 22 | return list; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /java-module/TrinityModuleWorld.java: -------------------------------------------------------------------------------- 1 | package wit_trinity_module; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.ArrayList; 5 | 6 | import org.teavm.interop.Memory; 7 | import org.teavm.interop.Address; 8 | import org.teavm.interop.Import; 9 | import org.teavm.interop.Export; 10 | import org.teavm.interop.CustomSection; 11 | 12 | public final class TrinityModuleWorld { 13 | private TrinityModuleWorld() {} 14 | 15 | @CustomSection(name = "component-type:TrinityModule") 16 | private static final String __WIT_BINDGEN_COMPONENT_TYPE = "01000061736d0a00010007b2010b400001006b73400105746f706963010073720207636f6e74656e747302746f737003400203636d647309617574686f722d6964730004400407636f6e74656e747309617574686f722d6964730b617574686f722d6e616d657304726f6f6d7300044000007742020203020107040872616e642d753634010040010173730100420602030201090405747261636501000405646562756701000404696e666f010004047761726e010004056572726f7201000a0d02037379730508036c6f67050a0b2a05076d657373616765030304696e697403000468656c7003020561646d696e0305066f6e2d6d73670306"; 17 | 18 | public static final int RETURN_AREA = Memory.malloc(8, 4).toInt(); 19 | } 20 | -------------------------------------------------------------------------------- /java-module/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | io.github.bnjbvr 6 | trinity-example-module 7 | 1.0-SNAPSHOT 8 | 9 | 10 | 9 11 | 0.2.4 12 | UTF-8 13 | 14 | 15 | 16 | 17 | 18 | com.fermyon 19 | teavm-classlib 20 | ${teavm.version} 21 | provided 22 | 23 | 24 | 25 | 26 | 27 | 28 | maven-compiler-plugin 29 | 3.1 30 | 31 | ${java.version} 32 | ${java.version} 33 | 34 | 35 | 36 | 37 | 38 | com.fermyon 39 | teavm-maven-plugin 40 | ${teavm.version} 41 | 42 | 43 | web-client 44 | 45 | compile 46 | 47 | 48 | ${project.build.directory}/generated/wasm/teavm-wasm 49 | WEBASSEMBLY 50 | 51 | wit_trinity_module.Log 52 | wit_trinity_module.Sys 53 | wit_trinity_module.TrinityModule 54 | wit_trinity_module.TrinityModuleImpl 55 | wit_trinity_module.TrinityModuleWorld 56 | 57 | true 58 | false 59 | 60 | SIMPLE 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /modules/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | 4 | [target.wasm32-unknown-unknown] 5 | rustflags = [ 6 | "-C", "target-feature=+simd128", # enable SIMD support for Wasm modules 7 | ] 8 | -------------------------------------------------------------------------------- /modules/.rustfmt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnjbvr/trinity/a5ff2b8b2158c7b7988c2dca5171ffcf60283b01/modules/.rustfmt -------------------------------------------------------------------------------- /modules/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.98" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 19 | 20 | [[package]] 21 | name = "horsejs" 22 | version = "0.1.0" 23 | dependencies = [ 24 | "libcommand", 25 | "serde", 26 | "serde_json", 27 | "wit-log", 28 | "wit-sync-request", 29 | ] 30 | 31 | [[package]] 32 | name = "itoa" 33 | version = "1.0.15" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 36 | 37 | [[package]] 38 | name = "libcommand" 39 | version = "0.1.0" 40 | dependencies = [ 41 | "wit-bindgen-rt", 42 | ] 43 | 44 | [[package]] 45 | name = "linkify" 46 | version = "0.1.0" 47 | dependencies = [ 48 | "anyhow", 49 | "libcommand", 50 | "regex", 51 | "serde", 52 | "shlex", 53 | "textwrap-macros", 54 | "wit-kv", 55 | "wit-log", 56 | ] 57 | 58 | [[package]] 59 | name = "log" 60 | version = "0.4.27" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 63 | 64 | [[package]] 65 | name = "mastodon" 66 | version = "0.1.0" 67 | dependencies = [ 68 | "libcommand", 69 | "serde", 70 | "serde_json", 71 | "wit-kv", 72 | "wit-log", 73 | "wit-sync-request", 74 | ] 75 | 76 | [[package]] 77 | name = "memchr" 78 | version = "2.7.4" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 81 | 82 | [[package]] 83 | name = "memos" 84 | version = "0.1.0" 85 | dependencies = [ 86 | "libcommand", 87 | "serde", 88 | "serde_json", 89 | "wit-kv", 90 | "wit-log", 91 | "wit-sync-request", 92 | ] 93 | 94 | [[package]] 95 | name = "openai" 96 | version = "0.1.0" 97 | dependencies = [ 98 | "anyhow", 99 | "libcommand", 100 | "serde", 101 | "serde_json", 102 | "wit-kv", 103 | "wit-log", 104 | "wit-sync-request", 105 | ] 106 | 107 | [[package]] 108 | name = "proc-macro-hack" 109 | version = "0.5.20+deprecated" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" 112 | 113 | [[package]] 114 | name = "proc-macro2" 115 | version = "1.0.95" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 118 | dependencies = [ 119 | "unicode-ident", 120 | ] 121 | 122 | [[package]] 123 | name = "pun" 124 | version = "0.1.0" 125 | dependencies = [ 126 | "libcommand", 127 | "serde", 128 | "serde_json", 129 | "wit-log", 130 | "wit-sync-request", 131 | ] 132 | 133 | [[package]] 134 | name = "quote" 135 | version = "1.0.40" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 138 | dependencies = [ 139 | "proc-macro2", 140 | ] 141 | 142 | [[package]] 143 | name = "regex" 144 | version = "1.11.1" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 147 | dependencies = [ 148 | "aho-corasick", 149 | "memchr", 150 | "regex-automata", 151 | "regex-syntax", 152 | ] 153 | 154 | [[package]] 155 | name = "regex-automata" 156 | version = "0.4.9" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 159 | dependencies = [ 160 | "aho-corasick", 161 | "memchr", 162 | "regex-syntax", 163 | ] 164 | 165 | [[package]] 166 | name = "regex-syntax" 167 | version = "0.8.5" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 170 | 171 | [[package]] 172 | name = "ryu" 173 | version = "1.0.20" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 176 | 177 | [[package]] 178 | name = "secret" 179 | version = "0.1.0" 180 | dependencies = [ 181 | "libcommand", 182 | "wit-kv", 183 | "wit-log", 184 | ] 185 | 186 | [[package]] 187 | name = "serde" 188 | version = "1.0.219" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 191 | dependencies = [ 192 | "serde_derive", 193 | ] 194 | 195 | [[package]] 196 | name = "serde_derive" 197 | version = "1.0.219" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 200 | dependencies = [ 201 | "proc-macro2", 202 | "quote", 203 | "syn 2.0.100", 204 | ] 205 | 206 | [[package]] 207 | name = "serde_json" 208 | version = "1.0.140" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 211 | dependencies = [ 212 | "itoa", 213 | "memchr", 214 | "ryu", 215 | "serde", 216 | ] 217 | 218 | [[package]] 219 | name = "shlex" 220 | version = "1.3.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 223 | 224 | [[package]] 225 | name = "silverbullet" 226 | version = "0.1.0" 227 | dependencies = [ 228 | "libcommand", 229 | "serde", 230 | "serde_json", 231 | "wit-kv", 232 | "wit-log", 233 | "wit-sync-request", 234 | "wit-sys", 235 | ] 236 | 237 | [[package]] 238 | name = "smawk" 239 | version = "0.3.2" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" 242 | 243 | [[package]] 244 | name = "syn" 245 | version = "1.0.109" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 248 | dependencies = [ 249 | "proc-macro2", 250 | "quote", 251 | "unicode-ident", 252 | ] 253 | 254 | [[package]] 255 | name = "syn" 256 | version = "2.0.100" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 259 | dependencies = [ 260 | "proc-macro2", 261 | "quote", 262 | "unicode-ident", 263 | ] 264 | 265 | [[package]] 266 | name = "textwrap" 267 | version = "0.16.2" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 270 | dependencies = [ 271 | "smawk", 272 | "unicode-linebreak", 273 | "unicode-width", 274 | ] 275 | 276 | [[package]] 277 | name = "textwrap-macros" 278 | version = "0.3.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "975e7e5fec79db404c3f07c9182d1c4450d5e2c68340be6b5a7140f48b276a30" 281 | dependencies = [ 282 | "proc-macro-hack", 283 | "textwrap-macros-impl", 284 | ] 285 | 286 | [[package]] 287 | name = "textwrap-macros-impl" 288 | version = "0.3.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "32379e128f71c85438e4086388c6321232b64cd7e8560e2c2431d9bfc51fc3cc" 291 | dependencies = [ 292 | "proc-macro-hack", 293 | "quote", 294 | "syn 1.0.109", 295 | "textwrap", 296 | ] 297 | 298 | [[package]] 299 | name = "unicode-ident" 300 | version = "1.0.18" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 303 | 304 | [[package]] 305 | name = "unicode-linebreak" 306 | version = "0.1.5" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" 309 | 310 | [[package]] 311 | name = "unicode-width" 312 | version = "0.2.0" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 315 | 316 | [[package]] 317 | name = "uuid" 318 | version = "0.1.0" 319 | dependencies = [ 320 | "libcommand", 321 | "uuid 1.5.0", 322 | "wit-sys", 323 | ] 324 | 325 | [[package]] 326 | name = "uuid" 327 | version = "1.5.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" 330 | 331 | [[package]] 332 | name = "wit-bindgen-rt" 333 | version = "0.41.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" 336 | 337 | [[package]] 338 | name = "wit-kv" 339 | version = "0.1.0" 340 | dependencies = [ 341 | "anyhow", 342 | "serde", 343 | "serde_json", 344 | "wit-bindgen-rt", 345 | ] 346 | 347 | [[package]] 348 | name = "wit-log" 349 | version = "0.1.0" 350 | dependencies = [ 351 | "log", 352 | "wit-bindgen-rt", 353 | ] 354 | 355 | [[package]] 356 | name = "wit-sync-request" 357 | version = "0.1.0" 358 | dependencies = [ 359 | "log", 360 | "wit-bindgen-rt", 361 | ] 362 | 363 | [[package]] 364 | name = "wit-sys" 365 | version = "0.1.0" 366 | dependencies = [ 367 | "wit-bindgen-rt", 368 | ] 369 | -------------------------------------------------------------------------------- /modules/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | # Modules 4 | "./horsejs", 5 | "./linkify", 6 | "./pun", 7 | "./uuid", 8 | "./secret", 9 | "./mastodon", 10 | "./memos", 11 | "./openai", 12 | "./silverbullet", 13 | 14 | # Libs 15 | "./libcommand", 16 | "./wit-kv", 17 | "./wit-log", 18 | "./wit-sync-request", 19 | "./wit-sys", 20 | ] 21 | 22 | [workspace.dependencies] 23 | wit-bindgen = "0.41.0" 24 | wit-bindgen-rt = "0.41.0" 25 | 26 | libcommand = { path = "./libcommand" } 27 | wit-kv = { path = "./wit-kv" } 28 | wit-log = { path = "./wit-log" } 29 | wit-sync-request = { path = "./wit-sync-request" } 30 | wit-sys = { path = "./wit-sys" } 31 | -------------------------------------------------------------------------------- /modules/Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | build-tail: 4 | cargo build --target wasm32-unknown-unknown 5 | ./convert-to-component.sh 6 | 7 | install-tools: ## Install all the tools necessary to build the components. 8 | # Keep those values in sync with the main CI workflow! 9 | cargo install --locked wit-bindgen-cli@0.41.0 --force 10 | cargo install --locked wasm-tools@1.229.0 --force 11 | 12 | bindings: ## Regenerate the Rust bindings from the WIT files, for the libraries. 13 | # Generate Rust bindings for each library. 14 | wit-bindgen rust ../wit/kv.wit --out-dir wit-kv/src/ --format --runtime-path wit_bindgen_rt 15 | wit-bindgen rust ../wit/log.wit --out-dir wit-log/src/ --format --runtime-path wit_bindgen_rt 16 | wit-bindgen rust ../wit/sync-request.wit --out-dir wit-sync-request/src/ --format --runtime-path wit_bindgen_rt 17 | wit-bindgen rust ../wit/sys.wit --out-dir wit-sys/src/ --format --runtime-path wit_bindgen_rt 18 | 19 | # Generate Rust bindings for the export library. 20 | wit-bindgen rust ../wit/trinity-module.wit --out-dir libcommand/src/ --format --runtime-path wit_bindgen_rt --pub-export-macro 21 | 22 | build: bindings build-tail ## Build all the component modules in debug mode. 23 | @echo 24 | 25 | release: bindings 26 | @echo "Building all the component modules in release mode..." 27 | cargo build --target wasm32-unknown-unknown --release 28 | ./convert-to-component.sh 29 | 30 | check: bindings ## Check all the component modules in debug mode. 31 | cargo check --target wasm32-unknown-unknown 32 | 33 | watch: bindings ## Regenerates bindings once and watches for changes in the Rust source files. 34 | cargo watch -s "make build-tail" 35 | 36 | clean: ## Gets rid of the directory target. 37 | cargo clean 38 | 39 | help: ## Show the help. 40 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 41 | -------------------------------------------------------------------------------- /modules/README.md: -------------------------------------------------------------------------------- 1 | # modules 2 | 3 | These are the Trinity modules. 4 | 5 | Install all the required tools with `./install.sh`. 6 | Then use any of `./check.sh`, `./build.sh`. 7 | 8 | `regenerate.sh` can be used to regenerate the wit bindings into the library directories. 9 | -------------------------------------------------------------------------------- /modules/convert-to-component.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | for path in target/wasm32-unknown-unknown/**/*.wasm; do 4 | echo "Generating component for $path" 5 | wasm-tools component new $path -o $path 6 | done 7 | -------------------------------------------------------------------------------- /modules/horsejs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "horsejs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.147", features = ["derive"] } 8 | serde_json = "1.0.87" 9 | 10 | libcommand.workspace = true 11 | wit-log.workspace = true 12 | wit-sync-request.workspace = true 13 | 14 | [lib] 15 | crate-type = ["cdylib"] 16 | 17 | [package.metadata.component] 18 | target.path = "../../wit/trinity-module.wit" 19 | -------------------------------------------------------------------------------- /modules/horsejs/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use libcommand::{impl_command, CommandClient, TrinityCommand}; 4 | use wit_log as log; 5 | use wit_sync_request; 6 | 7 | struct Component; 8 | 9 | impl Component { 10 | fn get_quote(msg: &str) -> Option { 11 | if !msg.starts_with("!horsejs") { 12 | return None; 13 | } 14 | 15 | const URL: &str = "https://javascript.horse/random.json"; 16 | 17 | let resp = wit_sync_request::Request::get(URL) 18 | .header("Accept", "application/json") 19 | .run() 20 | .ok()?; 21 | 22 | if resp.status != wit_sync_request::ResponseStatus::Success { 23 | log::info!("request failed with non-success status code"); 24 | } 25 | 26 | #[derive(serde::Deserialize)] 27 | struct Response { 28 | text: String, 29 | } 30 | 31 | serde_json::from_str::(&resp.body?) 32 | .ok() 33 | .map(|resp| resp.text) 34 | } 35 | } 36 | 37 | impl TrinityCommand for Component { 38 | fn init(_config: HashMap) { 39 | let _ = log::set_boxed_logger(Box::new(crate::log::WitLog::new())); 40 | log::set_max_level(log::LevelFilter::Trace); 41 | log::trace!("Called the init() method \\o/"); 42 | } 43 | 44 | fn on_help(_topic: Option<&str>) -> String { 45 | "Contextless twitter quotes about the JavaScript".to_owned() 46 | } 47 | 48 | fn on_msg(client: &mut CommandClient, content: &str) { 49 | if let Some(content) = Self::get_quote(&content) { 50 | client.respond(content); 51 | } 52 | } 53 | } 54 | 55 | impl_command!(); 56 | -------------------------------------------------------------------------------- /modules/libcommand/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libcommand" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wit-bindgen-rt.workspace = true 8 | -------------------------------------------------------------------------------- /modules/libcommand/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! High-level library providing a trait that, once implemented, hides the complexity of 2 | //! Wit bindings. 3 | 4 | use std::collections::HashMap; 5 | use trinity_module::exports::trinity::module; 6 | 7 | pub mod trinity_module; 8 | pub use trinity_module::export; 9 | 10 | /// Implements a command for a given type, assuming the type implements the `TrinityCommand` trait. 11 | #[macro_export] 12 | macro_rules! impl_command { 13 | () => { 14 | $crate::export!(Component with_types_in $crate::trinity_module); 15 | }; 16 | } 17 | 18 | fn consume_client(client: CommandClient) -> Vec { 19 | let mut actions = Vec::new(); 20 | 21 | actions.extend(client.messages.into_iter().map(|msg| { 22 | module::messaging::Action::Respond(module::messaging::Message { 23 | text: msg.1, 24 | html: None, 25 | to: msg.0 .0, 26 | }) 27 | })); 28 | 29 | actions.extend( 30 | client 31 | .reactions 32 | .into_iter() 33 | .map(|reaction| module::messaging::Action::React(reaction)), 34 | ); 35 | 36 | actions 37 | } 38 | 39 | // Implement `TrinityCommand`, and get an implementation of `Guest` for free! 40 | impl module::messaging::Guest for T { 41 | fn init(config: Option>) { 42 | // Convert the Vec of tuples to a HashMap for convenience. 43 | let config = match config { 44 | Some(cfg) => cfg 45 | .iter() 46 | .map(|(k, v)| (k.to_string(), v.to_string())) 47 | .collect(), 48 | None => HashMap::new(), 49 | }; 50 | 51 | Self::init(config); 52 | } 53 | 54 | fn help(topic: Option) -> String { 55 | Self::on_help(topic.as_deref()) 56 | } 57 | 58 | fn on_msg( 59 | content: String, 60 | author_id: String, 61 | _author_name: String, 62 | room: String, 63 | ) -> Vec { 64 | let mut client = CommandClient::new(room, author_id.clone()); 65 | Self::on_msg(&mut client, &content); 66 | consume_client(client) 67 | } 68 | 69 | fn admin(cmd: String, author_id: String, room: String) -> Vec { 70 | let mut client = CommandClient::new(room.clone(), author_id); 71 | Self::on_admin(&mut client, &cmd); 72 | consume_client(client) 73 | } 74 | } 75 | 76 | pub struct Recipient(pub String); 77 | 78 | pub struct CommandClient { 79 | inbound_msg_room: String, 80 | inbound_msg_author: String, 81 | pub messages: Vec<(Recipient, String)>, 82 | pub reactions: Vec, 83 | } 84 | 85 | impl CommandClient { 86 | pub fn new(room: String, author: String) -> Self { 87 | Self { 88 | inbound_msg_room: room, 89 | inbound_msg_author: author, 90 | messages: Default::default(), 91 | reactions: Default::default(), 92 | } 93 | } 94 | 95 | /// Who sent the original message we're reacting to? 96 | pub fn from(&self) -> &str { 97 | &self.inbound_msg_author 98 | } 99 | 100 | /// Indicates in which room this message has been received. 101 | pub fn room(&self) -> &str { 102 | &self.inbound_msg_room 103 | } 104 | 105 | /// Queues a message to be sent to the author of the original message. 106 | pub fn respond(&mut self, msg: impl Into) { 107 | self.respond_to(msg.into(), self.inbound_msg_author.clone()) 108 | } 109 | 110 | /// Queues a message to be sent to someone else. 111 | pub fn respond_to(&mut self, msg: String, author: String) { 112 | self.messages.push((Recipient(author), msg)); 113 | } 114 | 115 | pub fn react_with(&mut self, reaction: String) { 116 | self.reactions.push(reaction); 117 | } 118 | 119 | pub fn react_with_ok(&mut self) { 120 | self.react_with("👌".to_owned()); 121 | } 122 | } 123 | 124 | pub trait TrinityCommand { 125 | /// Code that will be called once during initialization of the command. This is a good time to 126 | /// retrieve settings from the database and cache them locally, if needs be, or run any 127 | /// initialization code that shouldn't run on every message later. 128 | fn init(_config: HashMap) {} 129 | 130 | /// Handle a message received in a room where the bot is present. 131 | /// 132 | /// The message isn't identified as a request for help or an admin command. Those are handled 133 | /// respectively by `on_help` and `on_admin`. 134 | /// 135 | /// This should always be implemented, otherwise the command doesn't do anything. 136 | fn on_msg(client: &mut CommandClient, content: &str); 137 | 138 | /// Respond to a help request, for this specific command. 139 | /// 140 | /// If the topic is not set, then this should return a general description of the command, with 141 | /// hints to the possible topics. If the topic is set, then this function should document 142 | /// something related to the specific topic. 143 | /// 144 | /// This should always be implemented, at least to document what's the command's purpose. 145 | fn on_help(_topic: Option<&str>) -> String; 146 | 147 | /// Handle a message received by an admin, prefixed with the `!admin` subject. 148 | /// 149 | /// By default this does nothing, as admin commands are facultative. 150 | fn on_admin(_client: &mut CommandClient, _command: &str) {} 151 | } 152 | -------------------------------------------------------------------------------- /modules/linkify/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "linkify" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.66" 8 | regex = "1" 9 | serde = { version = "1.0.147", features = ["derive"] } 10 | shlex = "1.3.0" 11 | textwrap-macros = "0.3.0" 12 | 13 | libcommand.workspace = true 14 | wit-kv.workspace = true 15 | wit-log.workspace = true 16 | 17 | [lib] 18 | crate-type = ["cdylib"] 19 | 20 | [package.metadata.component] 21 | target.path = "../../wit/trinity-module.wit" 22 | -------------------------------------------------------------------------------- /modules/linkify/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use anyhow::Context as _; 4 | use libcommand::{impl_command, CommandClient, TrinityCommand}; 5 | use regex::Regex; 6 | use serde::{Deserialize, Serialize}; 7 | use shlex; 8 | use textwrap_macros::dedent; 9 | 10 | use wit_log as log; 11 | 12 | #[derive(Debug, Default, Deserialize, Serialize)] 13 | struct Rule { 14 | name: String, 15 | re: String, 16 | sub: String, 17 | } 18 | 19 | impl TryFrom<&str> for Rule { 20 | type Error = String; 21 | 22 | fn try_from(cmd: &str) -> anyhow::Result { 23 | let words = shlex::split(&cmd).unwrap_or_default(); 24 | if words.len() != 3 { 25 | return Err(String::from("Three inputs expected: name/re/sub")); 26 | } 27 | 28 | Ok(Self { 29 | name: words[0].clone(), 30 | re: words[1].clone(), 31 | sub: words[2].clone(), 32 | }) 33 | } 34 | } 35 | 36 | #[derive(Debug, Default, Deserialize, Serialize)] 37 | struct RoomConfig { 38 | enabled_rules: HashSet, 39 | } 40 | 41 | struct Component; 42 | 43 | impl Component { 44 | fn get_rules() -> Vec { 45 | wit_kv::get::<_, Vec>("rules") 46 | .ok() 47 | .flatten() 48 | .unwrap_or_default() 49 | } 50 | 51 | fn get_room_config(room: &str) -> RoomConfig { 52 | wit_kv::get::<_, RoomConfig>(&format!("room:{}", room)) 53 | .ok() 54 | .flatten() 55 | .unwrap_or_default() 56 | } 57 | 58 | fn replace(msg: &str, rc: RoomConfig) -> Option { 59 | for rule in Self::get_rules() 60 | .iter() 61 | .filter(|&r| rc.enabled_rules.contains(&r.name)) 62 | { 63 | let Ok(re) = Regex::new(&rule.re) else { 64 | // Shouldn't happen in theory, since the rules are validated at creation. 65 | log::warn!("unexpected invalid regex in replace()"); 66 | continue; 67 | }; 68 | if let Some(caps) = re.captures(msg) { 69 | let mut dest = String::new(); 70 | caps.expand(&rule.sub, &mut dest); 71 | return Some(dest); 72 | } 73 | } 74 | None 75 | } 76 | 77 | fn handle_admin(cmd: &str, _sender: &str, room: &str) -> anyhow::Result { 78 | // Format: new NAME RE SUB 79 | if let Some(input) = cmd.strip_prefix("new") { 80 | let rule = match Rule::try_from(input) { 81 | Ok(r) => r, 82 | Err(e) => return Ok(format!("Error parsing rule: {}", e)), 83 | }; 84 | 85 | let mut rules = Self::get_rules(); 86 | 87 | // Don't overwrite existing rules 88 | if rules.iter().any(|r| r.name == rule.name) { 89 | return Ok(format!("Rule '{}' already exists!", &rule.name)); 90 | } 91 | 92 | // Ensure the regex is valid 93 | if Regex::new(&rule.re).is_err() { 94 | return Ok(format!("Invalid regex `{}`!", &rule.re)); 95 | } 96 | 97 | rules.push(rule); 98 | let _ = wit_kv::set("rules", &rules); 99 | return Ok("Rule has been created!".into()); 100 | } 101 | 102 | // Format: delete NAME 103 | if let Some(cmd) = cmd.strip_prefix("delete") { 104 | let mut split = cmd.trim().split_whitespace(); 105 | let name = split.next().context("missing name")?; 106 | let mut rules = Self::get_rules(); 107 | if let Some(index) = rules.iter().position(|r| r.name == name) { 108 | rules.remove(index); 109 | let _ = wit_kv::set("rules", &rules); 110 | return Ok("Rule has been deleted!".into()); 111 | } 112 | return Ok(format!("Rule '{}' not found!", &name)); 113 | } 114 | 115 | // Format: enable NAME 116 | if let Some(cmd) = cmd.strip_prefix("enable") { 117 | let mut split = cmd.trim().split_whitespace(); 118 | let name = split.next().context("missing name")?; 119 | let rules = Self::get_rules(); 120 | if rules.iter().any(|r| r.name == name) { 121 | let mut rc = Self::get_room_config(room); 122 | if rc.enabled_rules.contains(name) { 123 | return Ok(format!("Rule '{}' is already enabled!", &name)); 124 | } 125 | rc.enabled_rules.insert(name.to_string()); 126 | let _ = wit_kv::set(&format!("room:{}", &room), &rc); 127 | return Ok(format!("Rule '{}' has been enabled!", &name)); 128 | } 129 | return Ok(format!("Rule '{}' not found!", &name)); 130 | } 131 | 132 | // Format: disable NAME 133 | if let Some(cmd) = cmd.strip_prefix("disable") { 134 | let mut split = cmd.trim().split_whitespace(); 135 | let name = split.next().context("missing name")?; 136 | let rules = Self::get_rules(); 137 | if rules.iter().any(|r| r.name == name) { 138 | let mut rc = Self::get_room_config(room); 139 | if rc.enabled_rules.remove(name) { 140 | let _ = wit_kv::set(&format!("room:{}", &room), &rc); 141 | return Ok(format!("Rule '{}' has been disabled!", &name)); 142 | } 143 | return Ok(format!("Rule '{}' is already disabled!", &name)); 144 | } 145 | return Ok(format!("Rule '{}' not found!", &name)); 146 | } 147 | 148 | // Format: list 149 | if cmd == "list" { 150 | let rules = Self::get_rules(); 151 | if rules.is_empty() { 152 | return Ok("No rules found.".to_string()); 153 | } 154 | let mut msg = String::new(); 155 | for rule in rules { 156 | msg.push_str(&format!("\n{:?}", &rule)); 157 | } 158 | return Ok(msg); 159 | } 160 | 161 | Ok(format!( 162 | "Unknown command '{}'!", 163 | cmd.split_whitespace().next().unwrap_or("none") 164 | )) 165 | } 166 | } 167 | 168 | impl TrinityCommand for Component { 169 | fn init(_config: HashMap) { 170 | let _ = log::set_boxed_logger(Box::new(log::WitLog::new())); 171 | log::set_max_level(log::LevelFilter::Trace); 172 | log::trace!("Called the init() method \\o/"); 173 | } 174 | 175 | fn on_help(topic: Option<&str>) -> String { 176 | if let Some(topic) = topic { 177 | match topic { 178 | "admin" => dedent!( 179 | r#" 180 | ### Command Overview 181 | 182 | Available admin commands: 183 | 184 | - new #NAME #RE #SUB 185 | - delete #NAME 186 | - enable #NAME 187 | - disable #NAME 188 | - list 189 | 190 | ### Creating and Enabling Rules 191 | 192 | Rules must first be created using the `new` command, then enabled on a 193 | room by room basis. To create a rule, run 194 | `!admin linkify new `. Where `` is an 195 | identifier to refer to the rule for future use, `` is a regular expression 196 | to match on text, and `` is a string into which the regex capture 197 | groups can be interpolated. 198 | 199 | For example, to create a new rule which links to a Github issue, run something 200 | like: 201 | 202 | !admin linkify new issue "(issue ?#?|# ?)([0-9]+)(\\s|$)" https://github.com/bnjbvr/trinity/issues/$2 203 | 204 | The `$2` will be substituted with the second regex capture group (which is the 205 | issue number in this example). It's also possible to use named capture groups, e.g: 206 | 207 | !admin linkify new issue "(issue ?#?|# ?)(?P[0-9]+)(\\s|$)" https://github.com/bnjbvr/trinity/issues/${issue} 208 | 209 | Then for each room you'd like this rule enabled, run: 210 | 211 | !admin linkify enable issue 212 | 213 | Now anytime someone types a string like issue 123 or #123, linkify will respond 214 | with the appropriate URL. 215 | 216 | The `disable` and `delete` commands take a single `` argument and will 217 | disable the rule, or delete it globally respectively. 218 | 219 | "# 220 | ) 221 | .into(), 222 | _ => "Invalid command!".into(), 223 | } 224 | } else { 225 | "Create regex based substition rules per channel! Help topics: admin".to_owned() 226 | } 227 | } 228 | 229 | fn on_msg(client: &mut CommandClient, content: &str) { 230 | if let Some(content) = Self::replace(&content, Self::get_room_config(client.room())) { 231 | client.respond(content); 232 | } 233 | } 234 | 235 | fn on_admin(client: &mut CommandClient, cmd: &str) { 236 | let content = match Self::handle_admin(&cmd, client.from(), client.room()) { 237 | Ok(resp) => resp, 238 | Err(err) => err.to_string(), 239 | }; 240 | client.respond(content); 241 | } 242 | } 243 | 244 | impl_command!(); 245 | -------------------------------------------------------------------------------- /modules/mastodon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mastodon" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.147", features = ["derive"] } 8 | serde_json = "1.0.87" 9 | 10 | libcommand.workspace = true 11 | wit-log.workspace = true 12 | wit-sync-request.workspace = true 13 | wit-kv.workspace = true 14 | 15 | [lib] 16 | crate-type = ["cdylib"] 17 | 18 | [package.metadata.component] 19 | target.path = "../../wit/trinity-module.wit" 20 | -------------------------------------------------------------------------------- /modules/mastodon/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use libcommand::{impl_command, CommandClient, TrinityCommand}; 4 | use wit_log as log; 5 | use wit_sync_request; 6 | 7 | #[derive(serde::Serialize, serde::Deserialize, Default)] 8 | struct RoomConfig { 9 | admins: Vec, 10 | token: String, 11 | base_url: String, 12 | } 13 | 14 | impl RoomConfig { 15 | fn is_admin(&self, author: &str) -> bool { 16 | self.admins.iter().any(|admin| *admin == author) 17 | } 18 | } 19 | 20 | struct Component; 21 | 22 | impl TrinityCommand for Component { 23 | fn init(_config: HashMap) { 24 | let _ = log::set_boxed_logger(Box::new(crate::log::WitLog::new())); 25 | log::set_max_level(log::LevelFilter::Trace); 26 | } 27 | 28 | fn on_help(topic: Option<&str>) -> String { 29 | if let Some(topic) = topic { 30 | match topic { 31 | "admin" => r#"available admin commands: 32 | - set-config #BASE_URL #TOKEN 33 | - remove-config 34 | - allow #USER_ID 35 | - disallow #USER_ID 36 | - list-posters"# 37 | .into(), 38 | "toot" | "!toot" => "Toot a message with !toot MESSAGE".into(), 39 | _ => "i don't know this command!".into(), 40 | } 41 | } else { 42 | "Post mastodon statuses from Matrix! Help topics: admin, toot".to_owned() 43 | } 44 | } 45 | 46 | fn on_msg(client: &mut CommandClient, content: &str) { 47 | let Some(content) = content.strip_prefix("!toot").map(|rest| rest.trim()) else { 48 | return; 49 | }; 50 | 51 | let author_id = client.from(); 52 | let content: &str = &content; 53 | let room = client.room(); 54 | 55 | let Ok(Some(mut config)) = wit_kv::get::<_, RoomConfig>(room) else { 56 | return client.respond("couldn't read room configuration (error or missing)"); 57 | }; 58 | 59 | if !config.is_admin(author_id) { 60 | return client.respond("you're not allowed to post, sorry!"); 61 | } 62 | 63 | if !config.base_url.ends_with("/") { 64 | config.base_url.push('/'); 65 | } 66 | config.base_url.push_str("api/v1/statuses"); 67 | 68 | #[derive(serde::Serialize)] 69 | struct Request { 70 | status: String, 71 | } 72 | 73 | let body = serde_json::to_string(&Request { 74 | status: content.to_owned(), 75 | }) 76 | .unwrap(); 77 | 78 | let Some(resp) = wit_sync_request::Request::post(&config.base_url) 79 | .header("Authorization", &format!("Bearer {}", config.token)) 80 | .header("Content-Type", "application/json") 81 | .body(&body) 82 | .run() 83 | .ok() 84 | else { 85 | return client.respond("didn't receive a response from the server"); 86 | }; 87 | 88 | if resp.status != wit_sync_request::ResponseStatus::Success { 89 | log::info!( 90 | "request failed with non-success status code:\n\t{:?}", 91 | resp.body 92 | ); 93 | return client.respond("error when sending toot, see logs!".to_owned()); 94 | } 95 | 96 | client.react_with_ok(); 97 | } 98 | 99 | fn on_admin(client: &mut CommandClient, cmd: &str) { 100 | let room = client.room(); 101 | let sender = client.from(); 102 | 103 | if let Some(rest) = cmd.strip_prefix("set-config") { 104 | // Format: set-config BASE_URL TOKEN 105 | let mut split = rest.trim().split_whitespace(); 106 | 107 | let Some(base_url) = split.next() else { 108 | return client.respond("missing base url"); 109 | }; 110 | let Some(token) = split.next() else { 111 | return client.respond("missing token"); 112 | }; 113 | 114 | let config = RoomConfig { 115 | admins: vec![sender.to_owned()], 116 | token: token.to_owned(), 117 | base_url: base_url.to_owned(), 118 | }; 119 | 120 | if let Err(err) = wit_kv::set(&room, &config) { 121 | return client.respond(format!("writing to kv store: {err:#}")); 122 | } 123 | 124 | return client.react_with_ok(); 125 | } 126 | 127 | if cmd.starts_with("remove-config") { 128 | // Format: remove-config 129 | if let Err(err) = wit_kv::remove(&room) { 130 | return client.respond(format!("writing to kv store: {err:#}")); 131 | } 132 | 133 | return client.react_with_ok(); 134 | } 135 | 136 | if let Some(rest) = cmd.strip_prefix("allow") { 137 | // Format: allow USER_ID 138 | let mut split = rest.trim().split_whitespace(); 139 | 140 | let Some(user_id) = split.next() else { 141 | return client.respond("missing user id"); 142 | }; 143 | 144 | let Ok(Some(mut current)) = wit_kv::get::<_, RoomConfig>(&room) else { 145 | return client.respond("couldn't read room config for room"); 146 | }; 147 | 148 | current.admins.push(user_id.to_owned()); 149 | 150 | if let Err(err) = wit_kv::set(&room, ¤t) { 151 | return client.respond(format!("when writing to kv store: {err:#}")); 152 | } 153 | 154 | return client.react_with_ok(); 155 | } 156 | 157 | if let Some(rest) = cmd.strip_prefix("disallow") { 158 | // Format: disallow USER_ID 159 | let mut split = rest.trim().split_whitespace(); 160 | 161 | let Some(user_id) = split.next() else { 162 | return client.respond("missing user id"); 163 | }; 164 | 165 | let Ok(Some(mut current)) = wit_kv::get::<_, RoomConfig>(&room) else { 166 | return client.respond("couldn't read room config for room"); 167 | }; 168 | 169 | if let Some(idx) = current.admins.iter().position(|val| val == user_id) { 170 | current.admins.remove(idx); 171 | } else { 172 | return client.respond("admin not found"); 173 | } 174 | 175 | if let Err(err) = wit_kv::set(&room, ¤t) { 176 | return client.respond(format!("when writing to kv store: {err:#}")); 177 | } 178 | 179 | return client.react_with_ok(); 180 | } 181 | 182 | if cmd.starts_with("list-posters") { 183 | // Format: list-posters ROOM 184 | let Ok(Some(current)) = wit_kv::get::<_, RoomConfig>(&room) else { 185 | return client.respond("couldn't read room config, or no config for this room"); 186 | }; 187 | return client.respond(current.admins.join(", ")); 188 | } 189 | 190 | client.respond("unknown command!"); 191 | } 192 | } 193 | 194 | impl_command!(); 195 | -------------------------------------------------------------------------------- /modules/memos/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "memos" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.147", features = ["derive"] } 8 | serde_json = "1.0.87" 9 | 10 | libcommand.workspace = true 11 | wit-log.workspace = true 12 | wit-sync-request.workspace = true 13 | wit-kv.workspace = true 14 | 15 | [lib] 16 | crate-type = ["cdylib"] 17 | 18 | [package.metadata.component] 19 | target.path = "../../wit/trinity-module.wit" 20 | -------------------------------------------------------------------------------- /modules/memos/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use libcommand::{impl_command, CommandClient, TrinityCommand}; 4 | use wit_log as log; 5 | use wit_sync_request; 6 | 7 | #[derive(serde::Serialize, serde::Deserialize)] 8 | struct RoomConfig { 9 | base_url: String, 10 | token: String, 11 | } 12 | 13 | struct Component; 14 | 15 | impl TrinityCommand for Component { 16 | fn init(_config: HashMap) { 17 | let _ = log::set_boxed_logger(Box::new(crate::log::WitLog::new())); 18 | log::set_max_level(log::LevelFilter::Trace); 19 | } 20 | 21 | fn on_help(topic: Option<&str>) -> String { 22 | if let Some(topic) = topic { 23 | match topic { 24 | "admin" | "!admin" => r#"available admin commands: 25 | - set-config #BASE_URL #TOKEN 26 | - remove-config"# 27 | .into(), 28 | "memo" | "!memo" => "send a memo with the !memo MARKDOWN CONTENT. The content can be multiline markdown, include tags, etc.".into(), 29 | _ => "i don't know this command!".into(), 30 | } 31 | } else { 32 | r#"Post memos to an instance of usememos.com from Matrix: 33 | 34 | 1. First configure with `!admin memos set-config` 35 | 2. Then send memos to your instance with `!memo CONTENT 36 | 3. ??? 37 | 4. Fun and profit!"# 38 | .to_owned() 39 | } 40 | } 41 | 42 | fn on_msg(client: &mut CommandClient, content: &str) { 43 | let Some(content) = content.strip_prefix("!memo").map(|rest| rest.trim()) else { 44 | return; 45 | }; 46 | 47 | let content: &str = &content; 48 | let room = client.room(); 49 | 50 | let mut config = match wit_kv::get::<_, RoomConfig>(room) { 51 | Ok(Some(config)) => config, 52 | Ok(None) => return client.respond("missing room configuration"), 53 | Err(err) => { 54 | log::error!("error when reading configuration: {err}"); 55 | return client.respond("error when reading configuration, check logs!"); 56 | } 57 | }; 58 | 59 | if !config.base_url.ends_with("/") { 60 | config.base_url.push('/'); 61 | } 62 | config.base_url.push_str("api/v1/memos"); 63 | 64 | #[derive(serde::Serialize)] 65 | struct Memo { 66 | content: String, 67 | visibility: String, 68 | } 69 | 70 | let body = serde_json::to_string(&Memo { 71 | content: content.to_owned(), 72 | visibility: "PRIVATE".to_owned(), 73 | }) 74 | .unwrap(); 75 | 76 | let Ok(resp) = wit_sync_request::Request::post(&config.base_url) 77 | .header("Authorization", &format!("Bearer {}", config.token)) 78 | .header("Content-Type", "application/json") 79 | .body(&body) 80 | .run() 81 | else { 82 | return client.respond("didn't receive a response from the server"); 83 | }; 84 | 85 | if resp.status != wit_sync_request::ResponseStatus::Success { 86 | log::info!( 87 | "request failed with non-success status code:\n\t{:?}", 88 | resp.body 89 | ); 90 | return client.respond("error when sending memo, see logs!".to_owned()); 91 | } 92 | 93 | client.react_with_ok(); 94 | } 95 | 96 | fn on_admin(client: &mut CommandClient, cmd: &str) { 97 | let room = client.room(); 98 | 99 | if let Some(rest) = cmd.strip_prefix("set-config") { 100 | // Format: set-config BASE_URL TOKEN 101 | let mut split = rest.trim().split_whitespace(); 102 | let Some(base_url) = split.next() else { 103 | return client.respond("missing base url"); 104 | }; 105 | let Some(token) = split.next() else { 106 | return client.respond("missing token"); 107 | }; 108 | let config = RoomConfig { 109 | token: token.to_owned(), 110 | base_url: base_url.to_owned(), 111 | }; 112 | if let Err(err) = wit_kv::set(&room, &config) { 113 | return client.respond(format!("writing to kv store: {err:#}")); 114 | } 115 | return client.react_with_ok(); 116 | } 117 | 118 | if cmd.starts_with("remove-config") { 119 | // Format: remove-config 120 | if let Err(err) = wit_kv::remove(&room) { 121 | return client.respond(format!("writing to kv store: {err:#}")); 122 | } 123 | return client.react_with_ok(); 124 | } 125 | 126 | client.respond("unknown command!"); 127 | } 128 | } 129 | 130 | impl_command!(); 131 | -------------------------------------------------------------------------------- /modules/openai/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openai" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.66" 8 | serde = { version = "1.0.147", features = ["derive"] } 9 | serde_json = "1.0.87" 10 | 11 | libcommand.workspace = true 12 | wit-log.workspace = true 13 | wit-sync-request.workspace = true 14 | wit-kv.workspace = true 15 | 16 | [lib] 17 | crate-type = ["cdylib"] 18 | 19 | [package.metadata.component] 20 | target.path = "../../wit/trinity-module.wit" 21 | -------------------------------------------------------------------------------- /modules/openai/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::Context as _; 4 | use libcommand::{impl_command, CommandClient, TrinityCommand}; 5 | use wit_log as log; 6 | use wit_sync_request; 7 | 8 | #[derive(serde::Serialize, serde::Deserialize)] 9 | enum TriggerMode { 10 | /// Every message will be handled by this bot command, unless another handler caught it first 11 | Always, 12 | 13 | //// Only messages starting with !ai prefix will be handled 14 | Prefix, 15 | } 16 | 17 | #[derive(serde::Serialize, serde::Deserialize)] 18 | struct RoomConfig { 19 | token: String, 20 | trigger: TriggerMode, 21 | } 22 | 23 | struct Component; 24 | 25 | const OPEN_AI_URL: &str = "https://api.openai.com/v1/completions"; 26 | 27 | impl Component { 28 | fn handle_msg(content: &str, room: &str) -> anyhow::Result> { 29 | let Some(config) = wit_kv::get::<_, RoomConfig>(room)? else { 30 | return Ok(None); 31 | }; 32 | 33 | match config.trigger { 34 | TriggerMode::Always => {} 35 | TriggerMode::Prefix => { 36 | if !content.starts_with("!ai") { 37 | return Ok(None); 38 | } 39 | } 40 | } 41 | 42 | #[derive(serde::Serialize)] 43 | struct Request<'a> { 44 | model: &'a str, 45 | prompt: &'a str, 46 | max_tokens: u32, 47 | temperature: f64, 48 | } 49 | 50 | let body = serde_json::to_string(&Request { 51 | model: "text-davinci-003", 52 | prompt: content, 53 | max_tokens: 128, 54 | temperature: 0.1, 55 | })?; 56 | 57 | let resp = wit_sync_request::Request::post(OPEN_AI_URL) 58 | .header("Authorization", &format!("Bearer {}", config.token)) 59 | .header("Content-Type", "application/json") 60 | .body(&body) 61 | .run() 62 | .ok() 63 | .context("no response")?; 64 | 65 | let resp_body = resp.body.context("missing response from OpenAI")?; 66 | 67 | log::trace!("received: {resp_body}"); 68 | 69 | #[allow(unused)] 70 | #[derive(serde::Deserialize)] 71 | struct OpenAiChoice { 72 | text: String, 73 | index: u32, 74 | log_probes: Option<()>, 75 | finish_reason: String, 76 | } 77 | 78 | #[derive(serde::Deserialize)] 79 | struct OpenAiResponse { 80 | choices: Vec, 81 | } 82 | 83 | let resp: OpenAiResponse = serde_json::from_str(&resp_body)?; 84 | 85 | if let Some(first_choice) = resp.choices.first() { 86 | Ok(Some(first_choice.text.trim().to_string())) 87 | } else { 88 | Ok(None) 89 | } 90 | } 91 | 92 | fn handle_admin(cmd: &str, room: &str) -> anyhow::Result { 93 | if let Some(rest) = cmd.strip_prefix("enable") { 94 | // Format: set-config TOKEN TRIGGER_MODE 95 | let Some((token, trigger)) = rest.trim().split_once(' ') else { 96 | anyhow::bail!("missing token or trigger mode"); 97 | }; 98 | 99 | let trigger = match trigger.trim() { 100 | "always" => TriggerMode::Always, 101 | "prefix" => TriggerMode::Prefix, 102 | _ => anyhow::bail!("unknown trigger mode, available: 'always' or 'trigger'"), 103 | }; 104 | 105 | let config = RoomConfig { 106 | token: token.to_owned(), 107 | trigger, 108 | }; 109 | wit_kv::set(&room, &config).context("writing to kv store")?; 110 | return Ok("added!".to_owned()); 111 | } 112 | 113 | if cmd.starts_with("disable") { 114 | // Format: remove-config 115 | wit_kv::remove(&room).context("writing to kv store")?; 116 | return Ok("removed config for that room!".to_owned()); 117 | } 118 | 119 | Ok("unknown command!".into()) 120 | } 121 | } 122 | 123 | impl TrinityCommand for Component { 124 | fn init(_config: HashMap) { 125 | let _ = log::set_boxed_logger(Box::new(crate::log::WitLog::new())); 126 | log::set_max_level(log::LevelFilter::Trace); 127 | } 128 | 129 | fn on_help(topic: Option<&str>) -> String { 130 | if let Some(topic) = topic { 131 | match topic { 132 | "admin" => r#"available admin commands: 133 | - enable #TOKEN #TRIGGER_MODE 134 | where TRIGGER_MODE is either: 135 | - 'always' (the bot will answer any message in that room) 136 | - 'prefix' (the bot will only handle messages starting with !ai) 137 | - disable"# 138 | .into(), 139 | _ => "i don't know this command!".into(), 140 | } 141 | } else { 142 | "Chat using OpenAI! Will respond to every message given it's configured in a room. Help topics: admin".to_owned() 143 | } 144 | } 145 | 146 | fn on_msg(client: &mut CommandClient, content: &str) { 147 | let content = match Self::handle_msg(&content, client.room()) { 148 | Ok(Some(resp)) => resp, 149 | Ok(None) => return, 150 | Err(err) => err.to_string(), 151 | }; 152 | client.respond(content); 153 | } 154 | 155 | fn on_admin(client: &mut CommandClient, cmd: &str) { 156 | let content = match Self::handle_admin(cmd, client.room()) { 157 | Ok(resp) => resp, 158 | Err(err) => err.to_string(), 159 | }; 160 | client.respond(content); 161 | } 162 | } 163 | 164 | impl_command!(); 165 | -------------------------------------------------------------------------------- /modules/pun/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pun" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.147", features = ["derive"] } 8 | serde_json = "1.0.87" 9 | 10 | libcommand.workspace = true 11 | wit-log.workspace = true 12 | wit-sync-request.workspace = true 13 | 14 | [lib] 15 | crate-type = ["cdylib"] 16 | 17 | [package.metadata.component] 18 | target.path = "../../wit/trinity-module.wit" 19 | -------------------------------------------------------------------------------- /modules/pun/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use libcommand::*; 4 | use wit_log as log; 5 | use wit_sync_request; 6 | 7 | struct Component; 8 | 9 | impl Component { 10 | fn get_pun(msg: &str) -> Option { 11 | if !msg.starts_with("!pun") { 12 | return None; 13 | } 14 | 15 | const URL: &str = "https://icanhazdadjoke.com/"; 16 | 17 | let resp = wit_sync_request::Request::get(URL) 18 | .header("Accept", "application/json") 19 | .run() 20 | .ok()?; 21 | 22 | if resp.status != wit_sync_request::ResponseStatus::Success { 23 | log::info!("request failed with non-success status code"); 24 | } 25 | 26 | #[derive(serde::Deserialize)] 27 | struct Response { 28 | joke: String, 29 | } 30 | 31 | serde_json::from_str::(&resp.body?) 32 | .ok() 33 | .map(|resp| resp.joke) 34 | } 35 | } 36 | 37 | impl TrinityCommand for Component { 38 | fn init(_config: HashMap) { 39 | let _ = log::set_boxed_logger(Box::new(log::WitLog::new())); 40 | log::set_max_level(log::LevelFilter::Trace); 41 | log::trace!("Called the init() method \\o/"); 42 | } 43 | 44 | fn on_msg(client: &mut CommandClient, msg: &str) { 45 | if let Some(content) = Self::get_pun(msg) { 46 | client.respond(content); 47 | } 48 | } 49 | 50 | fn on_help(topic: Option<&str>) -> String { 51 | if topic == Some("toxic") { 52 | "this is content fetched from a website on the internet, so this may be toxic!" 53 | } else { 54 | "Get radioactive puns straight from the internet! (ask '!help pun toxic' for details on radioactivity)" 55 | }.to_owned() 56 | } 57 | } 58 | 59 | impl_command!(); 60 | -------------------------------------------------------------------------------- /modules/secret/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "secret" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | libcommand.workspace = true 8 | wit-kv.workspace = true 9 | wit-log.workspace = true 10 | 11 | [lib] 12 | crate-type = ["cdylib"] 13 | 14 | [package.metadata.component] 15 | target.path = "../../wit/trinity-module.wit" 16 | -------------------------------------------------------------------------------- /modules/secret/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use libcommand::{impl_command, TrinityCommand}; 4 | use wit_log as log; 5 | 6 | struct Component; 7 | 8 | impl TrinityCommand for Component { 9 | fn init(_config: HashMap) { 10 | let _ = log::set_boxed_logger(Box::new(log::WitLog::new())); 11 | log::set_max_level(log::LevelFilter::Trace); 12 | log::trace!("Called the init() method \\o/"); 13 | } 14 | 15 | fn on_help(_topic: Option<&str>) -> String { 16 | "Secret tester".to_owned() 17 | } 18 | 19 | fn on_admin(client: &mut libcommand::CommandClient, cmd: &str) { 20 | match cmd.split_once(" ") { 21 | Some(("set", r)) => { 22 | if let Err(err) = wit_kv::set("secret", r) { 23 | log::error!("ohnoes! error when setting the secret value: {err:#}"); 24 | } else { 25 | client.react_with("👌".to_owned()); 26 | } 27 | } 28 | 29 | _ => { 30 | if cmd == "get" { 31 | let secret: Option = wit_kv::get("secret").unwrap_or_else(|err| { 32 | log::error!("couldn't read secret: {err:#}"); 33 | None 34 | }); 35 | client.respond(secret.unwrap_or_else(|| "".to_owned())); 36 | } else if cmd == "remove" { 37 | if let Err(err) = wit_kv::remove("secret") { 38 | log::error!("couldn't read value: {err:#}"); 39 | } else { 40 | client.react_with("🤯".to_owned()); 41 | }; 42 | } else { 43 | client.respond("i don't know this command??".to_owned()); 44 | } 45 | } 46 | } 47 | } 48 | 49 | fn on_msg(_client: &mut libcommand::CommandClient, _content: &str) { 50 | // Nothing! 51 | } 52 | } 53 | 54 | impl_command!(); 55 | -------------------------------------------------------------------------------- /modules/silverbullet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "silverbullet" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0.147", features = ["derive"] } 8 | serde_json = "1.0.87" 9 | 10 | libcommand.workspace = true 11 | wit-log.workspace = true 12 | wit-sync-request.workspace = true 13 | wit-kv.workspace = true 14 | wit-sys.workspace = true 15 | 16 | [lib] 17 | crate-type = ["cdylib"] 18 | 19 | [package.metadata.component] 20 | target.path = "../../wit/trinity-module.wit" 21 | -------------------------------------------------------------------------------- /modules/silverbullet/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use libcommand::{impl_command, CommandClient, TrinityCommand}; 4 | use wit_log as log; 5 | use wit_sync_request; 6 | 7 | #[derive(serde::Serialize, serde::Deserialize)] 8 | struct RoomConfig { 9 | base_url: String, 10 | token: String, 11 | } 12 | 13 | struct Component; 14 | 15 | impl TrinityCommand for Component { 16 | fn init(_config: HashMap) { 17 | let _ = log::set_boxed_logger(Box::new(crate::log::WitLog::new())); 18 | log::set_max_level(log::LevelFilter::Trace); 19 | } 20 | 21 | fn on_help(topic: Option<&str>) -> String { 22 | if let Some(topic) = topic { 23 | match topic { 24 | "admin" | "!admin" => r#"available admin commands: 25 | - set-config #BASE_URL #TOKEN 26 | - remove-config"# 27 | .into(), 28 | "sb" | "!sb" => "send a memo with the !sb TITLE CONTENT. The content can be multiline markdown, include tags, etc.".into(), 29 | _ => "i don't know this command!".into(), 30 | } 31 | } else { 32 | r#"Post memos to an instance of silverbullet.org from Matrix: 33 | 34 | 1. First configure with `!admin silverbullet set-config` 35 | 2. Then send memos to your instance with `!sb TITLE CONTENT 36 | 3. ??? 37 | 4. Fun and profit!"# 38 | .to_owned() 39 | } 40 | } 41 | 42 | fn on_msg(client: &mut CommandClient, content: &str) { 43 | let Some(content) = content.strip_prefix("!sb").map(|rest| rest.trim()) else { 44 | return; 45 | }; 46 | 47 | let mut split = content.split_whitespace(); 48 | let Some(title) = split.next().filter(|title| !title.is_empty()) else { 49 | return client.respond("missing title!"); 50 | }; 51 | 52 | let content = content.strip_prefix(title).unwrap().trim(); 53 | if content.is_empty() { 54 | return client.respond("no content to post!"); 55 | } 56 | 57 | let room = client.room(); 58 | 59 | let mut config = match wit_kv::get::<_, RoomConfig>(room) { 60 | Ok(Some(config)) => config, 61 | Ok(None) => return client.respond("missing room configuration"), 62 | Err(err) => { 63 | log::error!("error when reading configuration: {err}"); 64 | return client.respond("error when reading configuration, check logs!"); 65 | } 66 | }; 67 | 68 | if !config.base_url.ends_with("/") { 69 | config.base_url.push('/'); 70 | } 71 | 72 | let random = wit_sys::rand_u64(); 73 | let url = format!("Inbox/{title}_{random}.md"); 74 | log::trace!("about to send note to {url}"); 75 | config.base_url.push_str(&url); 76 | 77 | client.respond(format!("posting content to: {}", config.base_url)); 78 | 79 | let Ok(resp) = wit_sync_request::Request::put(&config.base_url) 80 | .header("Authorization", &format!("Bearer {}", config.token)) 81 | .body(&content) 82 | .run() 83 | else { 84 | return client.respond("didn't receive a response from the server"); 85 | }; 86 | 87 | if resp.status != wit_sync_request::ResponseStatus::Success { 88 | log::info!( 89 | "request failed with non-success status code:\n\t{:?}", 90 | resp.body 91 | ); 92 | return client.respond("error when sending memo, see logs!".to_owned()); 93 | } 94 | 95 | client.react_with_ok(); 96 | } 97 | 98 | fn on_admin(client: &mut CommandClient, cmd: &str) { 99 | let room = client.room(); 100 | 101 | if let Some(rest) = cmd.strip_prefix("set-config") { 102 | // Format: set-config BASE_URL TOKEN 103 | let mut split = rest.trim().split_whitespace(); 104 | let Some(base_url) = split.next() else { 105 | return client.respond("missing base url"); 106 | }; 107 | let Some(token) = split.next() else { 108 | return client.respond("missing token"); 109 | }; 110 | let config = RoomConfig { 111 | token: token.to_owned(), 112 | base_url: base_url.to_owned(), 113 | }; 114 | if let Err(err) = wit_kv::set(&room, &config) { 115 | return client.respond(format!("writing to kv store: {err:#}")); 116 | } 117 | return client.react_with_ok(); 118 | } 119 | 120 | if cmd.starts_with("remove-config") { 121 | // Format: remove-config 122 | if let Err(err) = wit_kv::remove(&room) { 123 | return client.respond(format!("writing to kv store: {err:#}")); 124 | } 125 | return client.react_with_ok(); 126 | } 127 | 128 | client.respond("unknown command!"); 129 | } 130 | } 131 | 132 | impl_command!(); 133 | -------------------------------------------------------------------------------- /modules/uuid/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uuid" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | uuid = "1.2.1" 8 | 9 | libcommand.workspace = true 10 | wit-sys.workspace = true 11 | 12 | [lib] 13 | crate-type = ["cdylib"] 14 | -------------------------------------------------------------------------------- /modules/uuid/src/lib.rs: -------------------------------------------------------------------------------- 1 | use libcommand::{impl_command, CommandClient}; 2 | 3 | struct Component; 4 | 5 | impl libcommand::TrinityCommand for Component { 6 | fn on_help(_topic: Option<&str>) -> String { 7 | "Simple uuid generator".to_owned() 8 | } 9 | 10 | fn on_msg(client: &mut CommandClient, content: &str) { 11 | if !content.starts_with("!uuid") { 12 | return; 13 | } 14 | 15 | let r1 = wit_sys::rand_u64(); 16 | let r2 = wit_sys::rand_u64(); 17 | let uuid = uuid::Uuid::from_u64_pair(r1, r2); 18 | 19 | let content = format!("{uuid}"); 20 | 21 | client.respond(content); 22 | } 23 | } 24 | 25 | impl_command!(); 26 | -------------------------------------------------------------------------------- /modules/wit-kv/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wit-kv" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wit-bindgen-rt.workspace = true 8 | serde = "1.0.147" 9 | serde_json = "1.0.87" 10 | anyhow = "1.0.66" 11 | 12 | [lib] 13 | -------------------------------------------------------------------------------- /modules/wit-kv/src/kv_world.rs: -------------------------------------------------------------------------------- 1 | // Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! 2 | // Options used: 3 | // * runtime_path: "wit_bindgen_rt" 4 | #[rustfmt::skip] 5 | #[allow(dead_code, clippy::all)] 6 | pub mod trinity { 7 | pub mod api { 8 | #[allow(dead_code, async_fn_in_trait, unused_imports, clippy::all)] 9 | pub mod kv { 10 | #[used] 11 | #[doc(hidden)] 12 | static __FORCE_SECTION_REF: fn() = super::super::super::__link_custom_section_describing_imports; 13 | use super::super::super::_rt; 14 | #[derive(Clone)] 15 | pub enum KvError { 16 | Internal(_rt::String), 17 | } 18 | impl ::core::fmt::Debug for KvError { 19 | fn fmt( 20 | &self, 21 | f: &mut ::core::fmt::Formatter<'_>, 22 | ) -> ::core::fmt::Result { 23 | match self { 24 | KvError::Internal(e) => { 25 | f.debug_tuple("KvError::Internal").field(e).finish() 26 | } 27 | } 28 | } 29 | } 30 | impl ::core::fmt::Display for KvError { 31 | fn fmt( 32 | &self, 33 | f: &mut ::core::fmt::Formatter<'_>, 34 | ) -> ::core::fmt::Result { 35 | write!(f, "{:?}", self) 36 | } 37 | } 38 | impl std::error::Error for KvError {} 39 | #[allow(unused_unsafe, clippy::all)] 40 | pub fn set(key: &[u8], value: &[u8]) -> Result<(), KvError> { 41 | unsafe { 42 | #[cfg_attr(target_pointer_width = "64", repr(align(8)))] 43 | #[cfg_attr(target_pointer_width = "32", repr(align(4)))] 44 | struct RetArea( 45 | [::core::mem::MaybeUninit< 46 | u8, 47 | >; 4 * ::core::mem::size_of::<*const u8>()], 48 | ); 49 | let mut ret_area = RetArea( 50 | [::core::mem::MaybeUninit::uninit(); 4 51 | * ::core::mem::size_of::<*const u8>()], 52 | ); 53 | let vec0 = key; 54 | let ptr0 = vec0.as_ptr().cast::(); 55 | let len0 = vec0.len(); 56 | let vec1 = value; 57 | let ptr1 = vec1.as_ptr().cast::(); 58 | let len1 = vec1.len(); 59 | let ptr2 = ret_area.0.as_mut_ptr().cast::(); 60 | #[cfg(target_arch = "wasm32")] 61 | #[link(wasm_import_module = "trinity:api/kv")] 62 | unsafe extern "C" { 63 | #[link_name = "set"] 64 | fn wit_import3( 65 | _: *mut u8, 66 | _: usize, 67 | _: *mut u8, 68 | _: usize, 69 | _: *mut u8, 70 | ); 71 | } 72 | #[cfg(not(target_arch = "wasm32"))] 73 | unsafe extern "C" fn wit_import3( 74 | _: *mut u8, 75 | _: usize, 76 | _: *mut u8, 77 | _: usize, 78 | _: *mut u8, 79 | ) { 80 | unreachable!() 81 | } 82 | unsafe { 83 | wit_import3(ptr0.cast_mut(), len0, ptr1.cast_mut(), len1, ptr2) 84 | }; 85 | let l4 = i32::from(*ptr2.add(0).cast::()); 86 | let result10 = match l4 { 87 | 0 => { 88 | let e = (); 89 | Ok(e) 90 | } 91 | 1 => { 92 | let e = { 93 | let l5 = i32::from( 94 | *ptr2.add(::core::mem::size_of::<*const u8>()).cast::(), 95 | ); 96 | let v9 = match l5 { 97 | n => { 98 | debug_assert_eq!(n, 0, "invalid enum discriminant"); 99 | let e9 = { 100 | let l6 = *ptr2 101 | .add(2 * ::core::mem::size_of::<*const u8>()) 102 | .cast::<*mut u8>(); 103 | let l7 = *ptr2 104 | .add(3 * ::core::mem::size_of::<*const u8>()) 105 | .cast::(); 106 | let len8 = l7; 107 | let bytes8 = _rt::Vec::from_raw_parts( 108 | l6.cast(), 109 | len8, 110 | len8, 111 | ); 112 | _rt::string_lift(bytes8) 113 | }; 114 | KvError::Internal(e9) 115 | } 116 | }; 117 | v9 118 | }; 119 | Err(e) 120 | } 121 | _ => _rt::invalid_enum_discriminant(), 122 | }; 123 | result10 124 | } 125 | } 126 | #[allow(unused_unsafe, clippy::all)] 127 | pub fn get(key: &[u8]) -> Result>, KvError> { 128 | unsafe { 129 | #[cfg_attr(target_pointer_width = "64", repr(align(8)))] 130 | #[cfg_attr(target_pointer_width = "32", repr(align(4)))] 131 | struct RetArea( 132 | [::core::mem::MaybeUninit< 133 | u8, 134 | >; 4 * ::core::mem::size_of::<*const u8>()], 135 | ); 136 | let mut ret_area = RetArea( 137 | [::core::mem::MaybeUninit::uninit(); 4 138 | * ::core::mem::size_of::<*const u8>()], 139 | ); 140 | let vec0 = key; 141 | let ptr0 = vec0.as_ptr().cast::(); 142 | let len0 = vec0.len(); 143 | let ptr1 = ret_area.0.as_mut_ptr().cast::(); 144 | #[cfg(target_arch = "wasm32")] 145 | #[link(wasm_import_module = "trinity:api/kv")] 146 | unsafe extern "C" { 147 | #[link_name = "get"] 148 | fn wit_import2(_: *mut u8, _: usize, _: *mut u8); 149 | } 150 | #[cfg(not(target_arch = "wasm32"))] 151 | unsafe extern "C" fn wit_import2(_: *mut u8, _: usize, _: *mut u8) { 152 | unreachable!() 153 | } 154 | unsafe { wit_import2(ptr0.cast_mut(), len0, ptr1) }; 155 | let l3 = i32::from(*ptr1.add(0).cast::()); 156 | let result13 = match l3 { 157 | 0 => { 158 | let e = { 159 | let l4 = i32::from( 160 | *ptr1.add(::core::mem::size_of::<*const u8>()).cast::(), 161 | ); 162 | match l4 { 163 | 0 => None, 164 | 1 => { 165 | let e = { 166 | let l5 = *ptr1 167 | .add(2 * ::core::mem::size_of::<*const u8>()) 168 | .cast::<*mut u8>(); 169 | let l6 = *ptr1 170 | .add(3 * ::core::mem::size_of::<*const u8>()) 171 | .cast::(); 172 | let len7 = l6; 173 | _rt::Vec::from_raw_parts(l5.cast(), len7, len7) 174 | }; 175 | Some(e) 176 | } 177 | _ => _rt::invalid_enum_discriminant(), 178 | } 179 | }; 180 | Ok(e) 181 | } 182 | 1 => { 183 | let e = { 184 | let l8 = i32::from( 185 | *ptr1.add(::core::mem::size_of::<*const u8>()).cast::(), 186 | ); 187 | let v12 = match l8 { 188 | n => { 189 | debug_assert_eq!(n, 0, "invalid enum discriminant"); 190 | let e12 = { 191 | let l9 = *ptr1 192 | .add(2 * ::core::mem::size_of::<*const u8>()) 193 | .cast::<*mut u8>(); 194 | let l10 = *ptr1 195 | .add(3 * ::core::mem::size_of::<*const u8>()) 196 | .cast::(); 197 | let len11 = l10; 198 | let bytes11 = _rt::Vec::from_raw_parts( 199 | l9.cast(), 200 | len11, 201 | len11, 202 | ); 203 | _rt::string_lift(bytes11) 204 | }; 205 | KvError::Internal(e12) 206 | } 207 | }; 208 | v12 209 | }; 210 | Err(e) 211 | } 212 | _ => _rt::invalid_enum_discriminant(), 213 | }; 214 | result13 215 | } 216 | } 217 | #[allow(unused_unsafe, clippy::all)] 218 | pub fn remove(key: &[u8]) -> Result<(), KvError> { 219 | unsafe { 220 | #[cfg_attr(target_pointer_width = "64", repr(align(8)))] 221 | #[cfg_attr(target_pointer_width = "32", repr(align(4)))] 222 | struct RetArea( 223 | [::core::mem::MaybeUninit< 224 | u8, 225 | >; 4 * ::core::mem::size_of::<*const u8>()], 226 | ); 227 | let mut ret_area = RetArea( 228 | [::core::mem::MaybeUninit::uninit(); 4 229 | * ::core::mem::size_of::<*const u8>()], 230 | ); 231 | let vec0 = key; 232 | let ptr0 = vec0.as_ptr().cast::(); 233 | let len0 = vec0.len(); 234 | let ptr1 = ret_area.0.as_mut_ptr().cast::(); 235 | #[cfg(target_arch = "wasm32")] 236 | #[link(wasm_import_module = "trinity:api/kv")] 237 | unsafe extern "C" { 238 | #[link_name = "remove"] 239 | fn wit_import2(_: *mut u8, _: usize, _: *mut u8); 240 | } 241 | #[cfg(not(target_arch = "wasm32"))] 242 | unsafe extern "C" fn wit_import2(_: *mut u8, _: usize, _: *mut u8) { 243 | unreachable!() 244 | } 245 | unsafe { wit_import2(ptr0.cast_mut(), len0, ptr1) }; 246 | let l3 = i32::from(*ptr1.add(0).cast::()); 247 | let result9 = match l3 { 248 | 0 => { 249 | let e = (); 250 | Ok(e) 251 | } 252 | 1 => { 253 | let e = { 254 | let l4 = i32::from( 255 | *ptr1.add(::core::mem::size_of::<*const u8>()).cast::(), 256 | ); 257 | let v8 = match l4 { 258 | n => { 259 | debug_assert_eq!(n, 0, "invalid enum discriminant"); 260 | let e8 = { 261 | let l5 = *ptr1 262 | .add(2 * ::core::mem::size_of::<*const u8>()) 263 | .cast::<*mut u8>(); 264 | let l6 = *ptr1 265 | .add(3 * ::core::mem::size_of::<*const u8>()) 266 | .cast::(); 267 | let len7 = l6; 268 | let bytes7 = _rt::Vec::from_raw_parts( 269 | l5.cast(), 270 | len7, 271 | len7, 272 | ); 273 | _rt::string_lift(bytes7) 274 | }; 275 | KvError::Internal(e8) 276 | } 277 | }; 278 | v8 279 | }; 280 | Err(e) 281 | } 282 | _ => _rt::invalid_enum_discriminant(), 283 | }; 284 | result9 285 | } 286 | } 287 | } 288 | } 289 | } 290 | #[rustfmt::skip] 291 | mod _rt { 292 | #![allow(dead_code, clippy::all)] 293 | pub use alloc_crate::string::String; 294 | pub use alloc_crate::vec::Vec; 295 | pub unsafe fn string_lift(bytes: Vec) -> String { 296 | if cfg!(debug_assertions) { 297 | String::from_utf8(bytes).unwrap() 298 | } else { 299 | String::from_utf8_unchecked(bytes) 300 | } 301 | } 302 | pub unsafe fn invalid_enum_discriminant() -> T { 303 | if cfg!(debug_assertions) { 304 | panic!("invalid enum discriminant") 305 | } else { 306 | unsafe { core::hint::unreachable_unchecked() } 307 | } 308 | } 309 | extern crate alloc as alloc_crate; 310 | } 311 | #[cfg(target_arch = "wasm32")] 312 | #[unsafe( 313 | link_section = "component-type:wit-bindgen:0.41.0:trinity:api:kv-world:encoded world" 314 | )] 315 | #[doc(hidden)] 316 | #[allow(clippy::octal_escapes)] 317 | pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 290] = *b"\ 318 | \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xa3\x01\x01A\x02\x01\ 319 | A\x02\x01B\x0c\x01q\x01\x08internal\x01s\0\x04\0\x08kv-error\x03\0\0\x01p}\x01j\0\ 320 | \x01\x01\x01@\x02\x03key\x02\x05value\x02\0\x03\x04\0\x03set\x01\x04\x01k\x02\x01\ 321 | j\x01\x05\x01\x01\x01@\x01\x03key\x02\0\x06\x04\0\x03get\x01\x07\x01@\x01\x03key\ 322 | \x02\0\x03\x04\0\x06remove\x01\x08\x03\0\x0etrinity:api/kv\x05\0\x04\0\x14trinit\ 323 | y:api/kv-world\x04\0\x0b\x0e\x01\0\x08kv-world\x03\0\0\0G\x09producers\x01\x0cpr\ 324 | ocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-rust\x060.41.0"; 325 | #[inline(never)] 326 | #[doc(hidden)] 327 | pub fn __link_custom_section_describing_imports() { 328 | wit_bindgen_rt::maybe_link_cabi_realloc(); 329 | } 330 | -------------------------------------------------------------------------------- /modules/wit-kv/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | 3 | mod kv_world; 4 | use kv_world::trinity::api::kv as wit; 5 | 6 | pub fn get serde::Deserialize<'a>>( 7 | key: &K, 8 | ) -> anyhow::Result> { 9 | let key = serde_json::to_vec(key).context("couldn't serialize get key")?; 10 | let val = wit::get(&key)?; 11 | if let Some(val) = val { 12 | let deser = serde_json::from_slice(&val).context("couldn't deserialize get value")?; 13 | Ok(Some(deser)) 14 | } else { 15 | Ok(None) 16 | } 17 | } 18 | 19 | pub fn remove(key: &T) -> anyhow::Result<()> { 20 | let key = serde_json::to_vec(key).context("couldn't serialize remove key")?; 21 | wit::remove(&key)?; 22 | Ok(()) 23 | } 24 | 25 | pub fn set( 26 | key: &T, 27 | val: &V, 28 | ) -> anyhow::Result<()> { 29 | let key = serde_json::to_vec(key).context("couldn't serialize set key")?; 30 | let val = serde_json::to_vec(val).context("couldn't serialize set value")?; 31 | wit::set(&key, &val)?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /modules/wit-log/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wit-log" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | log = { version = "0.4.17", features = ["std"] } 10 | wit-bindgen-rt.workspace = true 11 | 12 | [lib] 13 | -------------------------------------------------------------------------------- /modules/wit-log/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod log_world; 2 | use log_world::trinity::api::log; 3 | 4 | pub use ::log::*; 5 | 6 | /// A log implementation based on calls to the host. 7 | pub struct WitLog { 8 | enabled: bool, 9 | max_level: ::log::LevelFilter, 10 | } 11 | 12 | impl WitLog { 13 | pub fn new() -> Self { 14 | Self { 15 | enabled: true, 16 | max_level: ::log::LevelFilter::Trace, 17 | } 18 | } 19 | pub fn set_enabled(&mut self, enabled: bool) { 20 | self.enabled = enabled; 21 | } 22 | pub fn set_max_level(&mut self, level: ::log::LevelFilter) { 23 | self.max_level = level; 24 | } 25 | } 26 | 27 | impl ::log::Log for WitLog { 28 | fn enabled(&self, metadata: &::log::Metadata) -> bool { 29 | self.enabled && metadata.level().to_level_filter() <= self.max_level 30 | } 31 | 32 | fn log(&self, record: &::log::Record) { 33 | if !self.enabled(record.metadata()) { 34 | return; 35 | } 36 | 37 | let content = format!("{}", record.args()); 38 | match record.level().to_level_filter() { 39 | ::log::LevelFilter::Off => {} 40 | ::log::LevelFilter::Error => log::error(&content), 41 | ::log::LevelFilter::Warn => log::warn(&content), 42 | ::log::LevelFilter::Info => log::info(&content), 43 | ::log::LevelFilter::Debug => log::debug(&content), 44 | ::log::LevelFilter::Trace => log::trace(&content), 45 | } 46 | } 47 | 48 | fn flush(&self) { 49 | // nothing to do here 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /modules/wit-log/src/log_world.rs: -------------------------------------------------------------------------------- 1 | // Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! 2 | // Options used: 3 | // * runtime_path: "wit_bindgen_rt" 4 | #[rustfmt::skip] 5 | #[allow(dead_code, clippy::all)] 6 | pub mod trinity { 7 | pub mod api { 8 | #[allow(dead_code, async_fn_in_trait, unused_imports, clippy::all)] 9 | pub mod log { 10 | #[used] 11 | #[doc(hidden)] 12 | static __FORCE_SECTION_REF: fn() = super::super::super::__link_custom_section_describing_imports; 13 | #[allow(unused_unsafe, clippy::all)] 14 | pub fn trace(s: &str) -> () { 15 | unsafe { 16 | let vec0 = s; 17 | let ptr0 = vec0.as_ptr().cast::(); 18 | let len0 = vec0.len(); 19 | #[cfg(target_arch = "wasm32")] 20 | #[link(wasm_import_module = "trinity:api/log")] 21 | unsafe extern "C" { 22 | #[link_name = "trace"] 23 | fn wit_import1(_: *mut u8, _: usize); 24 | } 25 | #[cfg(not(target_arch = "wasm32"))] 26 | unsafe extern "C" fn wit_import1(_: *mut u8, _: usize) { 27 | unreachable!() 28 | } 29 | unsafe { wit_import1(ptr0.cast_mut(), len0) }; 30 | } 31 | } 32 | #[allow(unused_unsafe, clippy::all)] 33 | pub fn debug(s: &str) -> () { 34 | unsafe { 35 | let vec0 = s; 36 | let ptr0 = vec0.as_ptr().cast::(); 37 | let len0 = vec0.len(); 38 | #[cfg(target_arch = "wasm32")] 39 | #[link(wasm_import_module = "trinity:api/log")] 40 | unsafe extern "C" { 41 | #[link_name = "debug"] 42 | fn wit_import1(_: *mut u8, _: usize); 43 | } 44 | #[cfg(not(target_arch = "wasm32"))] 45 | unsafe extern "C" fn wit_import1(_: *mut u8, _: usize) { 46 | unreachable!() 47 | } 48 | unsafe { wit_import1(ptr0.cast_mut(), len0) }; 49 | } 50 | } 51 | #[allow(unused_unsafe, clippy::all)] 52 | pub fn info(s: &str) -> () { 53 | unsafe { 54 | let vec0 = s; 55 | let ptr0 = vec0.as_ptr().cast::(); 56 | let len0 = vec0.len(); 57 | #[cfg(target_arch = "wasm32")] 58 | #[link(wasm_import_module = "trinity:api/log")] 59 | unsafe extern "C" { 60 | #[link_name = "info"] 61 | fn wit_import1(_: *mut u8, _: usize); 62 | } 63 | #[cfg(not(target_arch = "wasm32"))] 64 | unsafe extern "C" fn wit_import1(_: *mut u8, _: usize) { 65 | unreachable!() 66 | } 67 | unsafe { wit_import1(ptr0.cast_mut(), len0) }; 68 | } 69 | } 70 | #[allow(unused_unsafe, clippy::all)] 71 | pub fn warn(s: &str) -> () { 72 | unsafe { 73 | let vec0 = s; 74 | let ptr0 = vec0.as_ptr().cast::(); 75 | let len0 = vec0.len(); 76 | #[cfg(target_arch = "wasm32")] 77 | #[link(wasm_import_module = "trinity:api/log")] 78 | unsafe extern "C" { 79 | #[link_name = "warn"] 80 | fn wit_import1(_: *mut u8, _: usize); 81 | } 82 | #[cfg(not(target_arch = "wasm32"))] 83 | unsafe extern "C" fn wit_import1(_: *mut u8, _: usize) { 84 | unreachable!() 85 | } 86 | unsafe { wit_import1(ptr0.cast_mut(), len0) }; 87 | } 88 | } 89 | #[allow(unused_unsafe, clippy::all)] 90 | pub fn error(s: &str) -> () { 91 | unsafe { 92 | let vec0 = s; 93 | let ptr0 = vec0.as_ptr().cast::(); 94 | let len0 = vec0.len(); 95 | #[cfg(target_arch = "wasm32")] 96 | #[link(wasm_import_module = "trinity:api/log")] 97 | unsafe extern "C" { 98 | #[link_name = "error"] 99 | fn wit_import1(_: *mut u8, _: usize); 100 | } 101 | #[cfg(not(target_arch = "wasm32"))] 102 | unsafe extern "C" fn wit_import1(_: *mut u8, _: usize) { 103 | unreachable!() 104 | } 105 | unsafe { wit_import1(ptr0.cast_mut(), len0) }; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | #[cfg(target_arch = "wasm32")] 112 | #[unsafe( 113 | link_section = "component-type:wit-bindgen:0.41.0:trinity:api:log-world:encoded world" 114 | )] 115 | #[doc(hidden)] 116 | #[allow(clippy::octal_escapes)] 117 | pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 238] = *b"\ 118 | \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07o\x01A\x02\x01A\x02\x01\ 119 | B\x06\x01@\x01\x01ss\x01\0\x04\0\x05trace\x01\0\x04\0\x05debug\x01\0\x04\0\x04in\ 120 | fo\x01\0\x04\0\x04warn\x01\0\x04\0\x05error\x01\0\x03\0\x0ftrinity:api/log\x05\0\ 121 | \x04\0\x15trinity:api/log-world\x04\0\x0b\x0f\x01\0\x09log-world\x03\0\0\0G\x09p\ 122 | roducers\x01\x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-rust\ 123 | \x060.41.0"; 124 | #[inline(never)] 125 | #[doc(hidden)] 126 | pub fn __link_custom_section_describing_imports() { 127 | wit_bindgen_rt::maybe_link_cabi_realloc(); 128 | } 129 | -------------------------------------------------------------------------------- /modules/wit-sync-request/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wit-sync-request" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | log = "0.4.17" 10 | wit-bindgen-rt.workspace = true 11 | 12 | [lib] 13 | -------------------------------------------------------------------------------- /modules/wit-sync-request/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod sync_request_world; 2 | use sync_request_world::trinity::api::sync_request as wit; 3 | 4 | use std::collections::HashMap; 5 | 6 | pub use wit::ResponseStatus; 7 | 8 | /// A log implementation based on calls to the host. 9 | pub struct Request { 10 | verb: wit::RequestVerb, 11 | url: String, 12 | headers: HashMap, 13 | body: Option, 14 | } 15 | 16 | impl Request { 17 | pub fn get(url: &str) -> Self { 18 | Self { 19 | verb: wit::RequestVerb::Get, 20 | url: url.to_owned(), 21 | headers: Default::default(), 22 | body: None, 23 | } 24 | } 25 | 26 | pub fn put(url: &str) -> Self { 27 | Self { 28 | verb: wit::RequestVerb::Put, 29 | url: url.to_owned(), 30 | headers: Default::default(), 31 | body: None, 32 | } 33 | } 34 | 35 | pub fn delete(url: &str) -> Self { 36 | Self { 37 | verb: wit::RequestVerb::Delete, 38 | url: url.to_owned(), 39 | headers: Default::default(), 40 | body: None, 41 | } 42 | } 43 | 44 | pub fn post(url: &str) -> Self { 45 | Self { 46 | verb: wit::RequestVerb::Post, 47 | url: url.to_owned(), 48 | headers: Default::default(), 49 | body: None, 50 | } 51 | } 52 | 53 | pub fn header(mut self, key: &str, val: &str) -> Self { 54 | let prev = self.headers.insert(key.to_owned(), val.to_owned()); 55 | if prev.is_some() { 56 | log::warn!("overriding header {}", key); 57 | } 58 | self 59 | } 60 | 61 | pub fn body(mut self, body: &str) -> Self { 62 | if self.body.is_some() { 63 | log::warn!("overriding request body"); 64 | } 65 | self.body = Some(body.to_owned()); 66 | self 67 | } 68 | 69 | pub fn run(self) -> Result { 70 | let headers: Vec<_> = self 71 | .headers 72 | .into_iter() 73 | .map(|(key, value)| wit::RequestHeader { key, value }) 74 | .collect(); 75 | let req = wit::Request { 76 | verb: self.verb, 77 | url: self.url, 78 | headers, 79 | body: self.body, 80 | }; 81 | wit::run_request(&req) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /modules/wit-sync-request/src/sync_request_world.rs: -------------------------------------------------------------------------------- 1 | // Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! 2 | // Options used: 3 | // * runtime_path: "wit_bindgen_rt" 4 | #[rustfmt::skip] 5 | #[allow(dead_code, clippy::all)] 6 | pub mod trinity { 7 | pub mod api { 8 | #[allow(dead_code, async_fn_in_trait, unused_imports, clippy::all)] 9 | pub mod sync_request { 10 | #[used] 11 | #[doc(hidden)] 12 | static __FORCE_SECTION_REF: fn() = super::super::super::__link_custom_section_describing_imports; 13 | use super::super::super::_rt; 14 | #[repr(u8)] 15 | #[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] 16 | pub enum RequestVerb { 17 | Get, 18 | Put, 19 | Delete, 20 | Post, 21 | } 22 | impl ::core::fmt::Debug for RequestVerb { 23 | fn fmt( 24 | &self, 25 | f: &mut ::core::fmt::Formatter<'_>, 26 | ) -> ::core::fmt::Result { 27 | match self { 28 | RequestVerb::Get => f.debug_tuple("RequestVerb::Get").finish(), 29 | RequestVerb::Put => f.debug_tuple("RequestVerb::Put").finish(), 30 | RequestVerb::Delete => { 31 | f.debug_tuple("RequestVerb::Delete").finish() 32 | } 33 | RequestVerb::Post => f.debug_tuple("RequestVerb::Post").finish(), 34 | } 35 | } 36 | } 37 | impl RequestVerb { 38 | #[doc(hidden)] 39 | pub unsafe fn _lift(val: u8) -> RequestVerb { 40 | if !cfg!(debug_assertions) { 41 | return ::core::mem::transmute(val); 42 | } 43 | match val { 44 | 0 => RequestVerb::Get, 45 | 1 => RequestVerb::Put, 46 | 2 => RequestVerb::Delete, 47 | 3 => RequestVerb::Post, 48 | _ => panic!("invalid enum discriminant"), 49 | } 50 | } 51 | } 52 | #[derive(Clone)] 53 | pub struct RequestHeader { 54 | pub key: _rt::String, 55 | pub value: _rt::String, 56 | } 57 | impl ::core::fmt::Debug for RequestHeader { 58 | fn fmt( 59 | &self, 60 | f: &mut ::core::fmt::Formatter<'_>, 61 | ) -> ::core::fmt::Result { 62 | f.debug_struct("RequestHeader") 63 | .field("key", &self.key) 64 | .field("value", &self.value) 65 | .finish() 66 | } 67 | } 68 | #[derive(Clone)] 69 | pub struct Request { 70 | pub verb: RequestVerb, 71 | pub url: _rt::String, 72 | pub headers: _rt::Vec, 73 | pub body: Option<_rt::String>, 74 | } 75 | impl ::core::fmt::Debug for Request { 76 | fn fmt( 77 | &self, 78 | f: &mut ::core::fmt::Formatter<'_>, 79 | ) -> ::core::fmt::Result { 80 | f.debug_struct("Request") 81 | .field("verb", &self.verb) 82 | .field("url", &self.url) 83 | .field("headers", &self.headers) 84 | .field("body", &self.body) 85 | .finish() 86 | } 87 | } 88 | #[repr(u8)] 89 | #[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] 90 | pub enum ResponseStatus { 91 | Success, 92 | Error, 93 | } 94 | impl ::core::fmt::Debug for ResponseStatus { 95 | fn fmt( 96 | &self, 97 | f: &mut ::core::fmt::Formatter<'_>, 98 | ) -> ::core::fmt::Result { 99 | match self { 100 | ResponseStatus::Success => { 101 | f.debug_tuple("ResponseStatus::Success").finish() 102 | } 103 | ResponseStatus::Error => { 104 | f.debug_tuple("ResponseStatus::Error").finish() 105 | } 106 | } 107 | } 108 | } 109 | impl ResponseStatus { 110 | #[doc(hidden)] 111 | pub unsafe fn _lift(val: u8) -> ResponseStatus { 112 | if !cfg!(debug_assertions) { 113 | return ::core::mem::transmute(val); 114 | } 115 | match val { 116 | 0 => ResponseStatus::Success, 117 | 1 => ResponseStatus::Error, 118 | _ => panic!("invalid enum discriminant"), 119 | } 120 | } 121 | } 122 | #[derive(Clone)] 123 | pub struct Response { 124 | pub status: ResponseStatus, 125 | pub body: Option<_rt::String>, 126 | } 127 | impl ::core::fmt::Debug for Response { 128 | fn fmt( 129 | &self, 130 | f: &mut ::core::fmt::Formatter<'_>, 131 | ) -> ::core::fmt::Result { 132 | f.debug_struct("Response") 133 | .field("status", &self.status) 134 | .field("body", &self.body) 135 | .finish() 136 | } 137 | } 138 | /// An error happened while trying to run a request. 139 | #[derive(Clone)] 140 | pub enum RunRequestError { 141 | /// The builder couldn't be created. 142 | Builder(_rt::String), 143 | /// The request couldn't be executed. 144 | Execute(_rt::String), 145 | } 146 | impl ::core::fmt::Debug for RunRequestError { 147 | fn fmt( 148 | &self, 149 | f: &mut ::core::fmt::Formatter<'_>, 150 | ) -> ::core::fmt::Result { 151 | match self { 152 | RunRequestError::Builder(e) => { 153 | f.debug_tuple("RunRequestError::Builder").field(e).finish() 154 | } 155 | RunRequestError::Execute(e) => { 156 | f.debug_tuple("RunRequestError::Execute").field(e).finish() 157 | } 158 | } 159 | } 160 | } 161 | impl ::core::fmt::Display for RunRequestError { 162 | fn fmt( 163 | &self, 164 | f: &mut ::core::fmt::Formatter<'_>, 165 | ) -> ::core::fmt::Result { 166 | write!(f, "{:?}", self) 167 | } 168 | } 169 | impl std::error::Error for RunRequestError {} 170 | #[allow(unused_unsafe, clippy::all)] 171 | pub fn run_request(req: &Request) -> Result { 172 | unsafe { 173 | #[cfg_attr(target_pointer_width = "64", repr(align(8)))] 174 | #[cfg_attr(target_pointer_width = "32", repr(align(4)))] 175 | struct RetArea( 176 | [::core::mem::MaybeUninit< 177 | u8, 178 | >; 5 * ::core::mem::size_of::<*const u8>()], 179 | ); 180 | let mut ret_area = RetArea( 181 | [::core::mem::MaybeUninit::uninit(); 5 182 | * ::core::mem::size_of::<*const u8>()], 183 | ); 184 | let Request { 185 | verb: verb0, 186 | url: url0, 187 | headers: headers0, 188 | body: body0, 189 | } = req; 190 | let vec1 = url0; 191 | let ptr1 = vec1.as_ptr().cast::(); 192 | let len1 = vec1.len(); 193 | let vec5 = headers0; 194 | let len5 = vec5.len(); 195 | let layout5 = _rt::alloc::Layout::from_size_align_unchecked( 196 | vec5.len() * (4 * ::core::mem::size_of::<*const u8>()), 197 | ::core::mem::size_of::<*const u8>(), 198 | ); 199 | let result5 = if layout5.size() != 0 { 200 | let ptr = _rt::alloc::alloc(layout5).cast::(); 201 | if ptr.is_null() { 202 | _rt::alloc::handle_alloc_error(layout5); 203 | } 204 | ptr 205 | } else { 206 | ::core::ptr::null_mut() 207 | }; 208 | for (i, e) in vec5.into_iter().enumerate() { 209 | let base = result5 210 | .add(i * (4 * ::core::mem::size_of::<*const u8>())); 211 | { 212 | let RequestHeader { key: key2, value: value2 } = e; 213 | let vec3 = key2; 214 | let ptr3 = vec3.as_ptr().cast::(); 215 | let len3 = vec3.len(); 216 | *base 217 | .add(::core::mem::size_of::<*const u8>()) 218 | .cast::() = len3; 219 | *base.add(0).cast::<*mut u8>() = ptr3.cast_mut(); 220 | let vec4 = value2; 221 | let ptr4 = vec4.as_ptr().cast::(); 222 | let len4 = vec4.len(); 223 | *base 224 | .add(3 * ::core::mem::size_of::<*const u8>()) 225 | .cast::() = len4; 226 | *base 227 | .add(2 * ::core::mem::size_of::<*const u8>()) 228 | .cast::<*mut u8>() = ptr4.cast_mut(); 229 | } 230 | } 231 | let (result7_0, result7_1, result7_2) = match body0 { 232 | Some(e) => { 233 | let vec6 = e; 234 | let ptr6 = vec6.as_ptr().cast::(); 235 | let len6 = vec6.len(); 236 | (1i32, ptr6.cast_mut(), len6) 237 | } 238 | None => (0i32, ::core::ptr::null_mut(), 0usize), 239 | }; 240 | let ptr8 = ret_area.0.as_mut_ptr().cast::(); 241 | #[cfg(target_arch = "wasm32")] 242 | #[link(wasm_import_module = "trinity:api/sync-request")] 243 | unsafe extern "C" { 244 | #[link_name = "run-request"] 245 | fn wit_import9( 246 | _: i32, 247 | _: *mut u8, 248 | _: usize, 249 | _: *mut u8, 250 | _: usize, 251 | _: i32, 252 | _: *mut u8, 253 | _: usize, 254 | _: *mut u8, 255 | ); 256 | } 257 | #[cfg(not(target_arch = "wasm32"))] 258 | unsafe extern "C" fn wit_import9( 259 | _: i32, 260 | _: *mut u8, 261 | _: usize, 262 | _: *mut u8, 263 | _: usize, 264 | _: i32, 265 | _: *mut u8, 266 | _: usize, 267 | _: *mut u8, 268 | ) { 269 | unreachable!() 270 | } 271 | unsafe { 272 | wit_import9( 273 | verb0.clone() as i32, 274 | ptr1.cast_mut(), 275 | len1, 276 | result5, 277 | len5, 278 | result7_0, 279 | result7_1, 280 | result7_2, 281 | ptr8, 282 | ) 283 | }; 284 | let l10 = i32::from(*ptr8.add(0).cast::()); 285 | let result24 = match l10 { 286 | 0 => { 287 | let e = { 288 | let l11 = i32::from( 289 | *ptr8.add(::core::mem::size_of::<*const u8>()).cast::(), 290 | ); 291 | let l12 = i32::from( 292 | *ptr8 293 | .add(2 * ::core::mem::size_of::<*const u8>()) 294 | .cast::(), 295 | ); 296 | Response { 297 | status: ResponseStatus::_lift(l11 as u8), 298 | body: match l12 { 299 | 0 => None, 300 | 1 => { 301 | let e = { 302 | let l13 = *ptr8 303 | .add(3 * ::core::mem::size_of::<*const u8>()) 304 | .cast::<*mut u8>(); 305 | let l14 = *ptr8 306 | .add(4 * ::core::mem::size_of::<*const u8>()) 307 | .cast::(); 308 | let len15 = l14; 309 | let bytes15 = _rt::Vec::from_raw_parts( 310 | l13.cast(), 311 | len15, 312 | len15, 313 | ); 314 | _rt::string_lift(bytes15) 315 | }; 316 | Some(e) 317 | } 318 | _ => _rt::invalid_enum_discriminant(), 319 | }, 320 | } 321 | }; 322 | Ok(e) 323 | } 324 | 1 => { 325 | let e = { 326 | let l16 = i32::from( 327 | *ptr8.add(::core::mem::size_of::<*const u8>()).cast::(), 328 | ); 329 | let v23 = match l16 { 330 | 0 => { 331 | let e23 = { 332 | let l17 = *ptr8 333 | .add(2 * ::core::mem::size_of::<*const u8>()) 334 | .cast::<*mut u8>(); 335 | let l18 = *ptr8 336 | .add(3 * ::core::mem::size_of::<*const u8>()) 337 | .cast::(); 338 | let len19 = l18; 339 | let bytes19 = _rt::Vec::from_raw_parts( 340 | l17.cast(), 341 | len19, 342 | len19, 343 | ); 344 | _rt::string_lift(bytes19) 345 | }; 346 | RunRequestError::Builder(e23) 347 | } 348 | n => { 349 | debug_assert_eq!(n, 1, "invalid enum discriminant"); 350 | let e23 = { 351 | let l20 = *ptr8 352 | .add(2 * ::core::mem::size_of::<*const u8>()) 353 | .cast::<*mut u8>(); 354 | let l21 = *ptr8 355 | .add(3 * ::core::mem::size_of::<*const u8>()) 356 | .cast::(); 357 | let len22 = l21; 358 | let bytes22 = _rt::Vec::from_raw_parts( 359 | l20.cast(), 360 | len22, 361 | len22, 362 | ); 363 | _rt::string_lift(bytes22) 364 | }; 365 | RunRequestError::Execute(e23) 366 | } 367 | }; 368 | v23 369 | }; 370 | Err(e) 371 | } 372 | _ => _rt::invalid_enum_discriminant(), 373 | }; 374 | if layout5.size() != 0 { 375 | _rt::alloc::dealloc(result5.cast(), layout5); 376 | } 377 | result24 378 | } 379 | } 380 | } 381 | } 382 | } 383 | #[rustfmt::skip] 384 | mod _rt { 385 | #![allow(dead_code, clippy::all)] 386 | pub use alloc_crate::string::String; 387 | pub use alloc_crate::vec::Vec; 388 | pub use alloc_crate::alloc; 389 | pub unsafe fn string_lift(bytes: Vec) -> String { 390 | if cfg!(debug_assertions) { 391 | String::from_utf8(bytes).unwrap() 392 | } else { 393 | String::from_utf8_unchecked(bytes) 394 | } 395 | } 396 | pub unsafe fn invalid_enum_discriminant() -> T { 397 | if cfg!(debug_assertions) { 398 | panic!("invalid enum discriminant") 399 | } else { 400 | unsafe { core::hint::unreachable_unchecked() } 401 | } 402 | } 403 | extern crate alloc as alloc_crate; 404 | } 405 | #[cfg(target_arch = "wasm32")] 406 | #[unsafe( 407 | link_section = "component-type:wit-bindgen:0.41.0:trinity:api:sync-request-world:encoded world" 408 | )] 409 | #[doc(hidden)] 410 | #[allow(clippy::octal_escapes)] 411 | pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 483] = *b"\ 412 | \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xda\x02\x01A\x02\x01\ 413 | A\x02\x01B\x11\x01m\x04\x03get\x03put\x06delete\x04post\x04\0\x0crequest-verb\x03\ 414 | \0\0\x01r\x02\x03keys\x05values\x04\0\x0erequest-header\x03\0\x02\x01p\x03\x01ks\ 415 | \x01r\x04\x04verb\x01\x03urls\x07headers\x04\x04body\x05\x04\0\x07request\x03\0\x06\ 416 | \x01m\x02\x07success\x05error\x04\0\x0fresponse-status\x03\0\x08\x01r\x02\x06sta\ 417 | tus\x09\x04body\x05\x04\0\x08response\x03\0\x0a\x01q\x02\x07builder\x01s\0\x07ex\ 418 | ecute\x01s\0\x04\0\x11run-request-error\x03\0\x0c\x01j\x01\x0b\x01\x0d\x01@\x01\x03\ 419 | req\x07\0\x0e\x04\0\x0brun-request\x01\x0f\x03\0\x18trinity:api/sync-request\x05\ 420 | \0\x04\0\x1etrinity:api/sync-request-world\x04\0\x0b\x18\x01\0\x12sync-request-w\ 421 | orld\x03\0\0\0G\x09producers\x01\x0cprocessed-by\x02\x0dwit-component\x070.227.1\ 422 | \x10wit-bindgen-rust\x060.41.0"; 423 | #[inline(never)] 424 | #[doc(hidden)] 425 | pub fn __link_custom_section_describing_imports() { 426 | wit_bindgen_rt::maybe_link_cabi_realloc(); 427 | } 428 | -------------------------------------------------------------------------------- /modules/wit-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wit-sys" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wit-bindgen-rt.workspace = true 8 | 9 | [lib] 10 | -------------------------------------------------------------------------------- /modules/wit-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod sys_world; 2 | 3 | pub use sys_world::trinity::api::sys::rand_u64; 4 | -------------------------------------------------------------------------------- /modules/wit-sys/src/sys_world.rs: -------------------------------------------------------------------------------- 1 | // Generated by `wit-bindgen` 0.41.0. DO NOT EDIT! 2 | // Options used: 3 | // * runtime_path: "wit_bindgen_rt" 4 | #[rustfmt::skip] 5 | #[allow(dead_code, clippy::all)] 6 | pub mod trinity { 7 | pub mod api { 8 | #[allow(dead_code, async_fn_in_trait, unused_imports, clippy::all)] 9 | pub mod sys { 10 | #[used] 11 | #[doc(hidden)] 12 | static __FORCE_SECTION_REF: fn() = super::super::super::__link_custom_section_describing_imports; 13 | #[allow(unused_unsafe, clippy::all)] 14 | pub fn rand_u64() -> u64 { 15 | unsafe { 16 | #[cfg(target_arch = "wasm32")] 17 | #[link(wasm_import_module = "trinity:api/sys")] 18 | unsafe extern "C" { 19 | #[link_name = "rand-u64"] 20 | fn wit_import0() -> i64; 21 | } 22 | #[cfg(not(target_arch = "wasm32"))] 23 | unsafe extern "C" fn wit_import0() -> i64 { 24 | unreachable!() 25 | } 26 | let ret = unsafe { wit_import0() }; 27 | ret as u64 28 | } 29 | } 30 | } 31 | } 32 | } 33 | #[cfg(target_arch = "wasm32")] 34 | #[unsafe( 35 | link_section = "component-type:wit-bindgen:0.41.0:trinity:api:sys-world:encoded world" 36 | )] 37 | #[doc(hidden)] 38 | #[allow(clippy::octal_escapes)] 39 | pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 200] = *b"\ 40 | \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07I\x01A\x02\x01A\x02\x01\ 41 | B\x02\x01@\0\0w\x04\0\x08rand-u64\x01\0\x03\0\x0ftrinity:api/sys\x05\0\x04\0\x15\ 42 | trinity:api/sys-world\x04\0\x0b\x0f\x01\0\x09sys-world\x03\0\0\0G\x09producers\x01\ 43 | \x0cprocessed-by\x02\x0dwit-component\x070.227.1\x10wit-bindgen-rust\x060.41.0"; 44 | #[inline(never)] 45 | #[doc(hidden)] 46 | pub fn __link_custom_section_describing_imports() { 47 | wit_bindgen_rt::maybe_link_cabi_realloc(); 48 | } 49 | -------------------------------------------------------------------------------- /src/admin_table.rs: -------------------------------------------------------------------------------- 1 | use redb::ReadableTable; 2 | 3 | use crate::ShareableDatabase; 4 | 5 | /// Name of the admin table. Can be kept internal. 6 | const ADMIN_TABLE: redb::TableDefinition = redb::TableDefinition::new("@admin"); 7 | 8 | /// Key for the `device_id` value in the admin table. 9 | pub const DEVICE_ID_ENTRY: &str = "device_id"; 10 | 11 | /// Reads a given key in the admin table from the database. 12 | /// 13 | /// Returns `Ok(None)` if the value wasn't present, `Ok(Some)` if it did exist. 14 | pub fn read(db: &ShareableDatabase, key: &str) -> anyhow::Result>> { 15 | let txn = db.begin_read()?; 16 | let table = match txn.open_table(ADMIN_TABLE) { 17 | Ok(table) => table, 18 | Err(err) => match err { 19 | redb::Error::DatabaseAlreadyOpen 20 | | redb::Error::InvalidSavepoint 21 | | redb::Error::Corrupted(_) 22 | | redb::Error::TableTypeMismatch(_) 23 | | redb::Error::DbSizeMismatch { .. } 24 | | redb::Error::TableAlreadyOpen(_, _) 25 | | redb::Error::OutOfSpace 26 | | redb::Error::Io(_) 27 | | redb::Error::LockPoisoned(_) => Err(err)?, 28 | redb::Error::TableDoesNotExist(_) => return Ok(None), 29 | }, 30 | }; 31 | Ok(table.get(key)?.map(|val| val.to_vec())) 32 | } 33 | 34 | /// Same as [`read`], but for a string value. 35 | pub fn read_str(db: &ShareableDatabase, key: &str) -> anyhow::Result> { 36 | match read(db, key)? { 37 | Some(bytes) => Ok(Some(String::from_utf8(bytes)?)), 38 | None => Ok(None), 39 | } 40 | } 41 | 42 | /// Writes a given key in the admin table from the database. 43 | pub fn write(db: &ShareableDatabase, key: &str, value: &[u8]) -> anyhow::Result<()> { 44 | let txn = db.begin_write()?; 45 | { 46 | let mut table = txn.open_table(ADMIN_TABLE)?; 47 | table.insert(key, value)?; 48 | } 49 | txn.commit()?; 50 | Ok(()) 51 | } 52 | 53 | /// Same as [`write`], but for a string ref. 54 | pub fn write_str(db: &ShareableDatabase, key: &str, value: &str) -> anyhow::Result<()> { 55 | write(db, key, value.as_bytes()) 56 | } 57 | -------------------------------------------------------------------------------- /src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow; 2 | use trinity::BotConfig; 3 | 4 | async fn real_main() -> anyhow::Result<()> { 5 | tracing_subscriber::fmt::init(); 6 | let config_path = std::env::args().nth(1); 7 | 8 | tracing::debug!("parsing config..."); 9 | // First check for a config file, then fallback to env if none found. 10 | let config = BotConfig::from_config(config_path).or_else(|_| BotConfig::from_env())?; 11 | 12 | tracing::debug!("creating client..."); 13 | trinity::run(config).await 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() -> anyhow::Result<()> { 18 | // just one trick to get rust-analyzer working in main :-) 19 | real_main().await 20 | } 21 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod admin_table; 2 | mod room_resolver; 3 | mod wasm; 4 | 5 | use anyhow::Context; 6 | use matrix_sdk::{ 7 | config::SyncSettings, 8 | event_handler::Ctx, 9 | room::Room, 10 | ruma::{ 11 | events::{ 12 | reaction::{ReactionEventContent, Relation}, 13 | room::{ 14 | member::StrippedRoomMemberEvent, 15 | message::{MessageType, RoomMessageEventContent, SyncRoomMessageEvent}, 16 | }, 17 | }, 18 | presence::PresenceState, 19 | OwnedUserId, RoomId, UserId, 20 | }, 21 | Client, 22 | }; 23 | use notify::{RecursiveMode, Watcher}; 24 | use room_resolver::RoomResolver; 25 | use serde::Deserialize; 26 | use std::{collections::HashMap, env, fs, path::PathBuf, sync::Arc}; 27 | use tokio::{ 28 | sync::Mutex, 29 | time::{sleep, Duration}, 30 | }; 31 | use tracing::{debug, error, info, trace, warn}; 32 | use wasm::{Module, WasmModules}; 33 | 34 | use crate::admin_table::DEVICE_ID_ENTRY; 35 | 36 | /// The configuration to run a trinity instance with. 37 | #[derive(Deserialize)] 38 | pub struct BotConfig { 39 | /// the matrix homeserver the bot should connect to. 40 | pub home_server: String, 41 | /// the user_id to be used on the homeserver. 42 | pub user_id: String, 43 | /// password to be used to log into the homeserver. 44 | pub password: String, 45 | /// where to store the matrix-sdk internal data. 46 | pub matrix_store_path: String, 47 | /// where to store the additional database data. 48 | pub redb_path: String, 49 | /// the admin user id for the bot. 50 | pub admin_user_id: OwnedUserId, 51 | /// paths where modules can be loaded. 52 | pub modules_paths: Vec, 53 | /// module specific configuration to forward to corresponding handler. 54 | pub modules_config: Option>>, 55 | } 56 | 57 | impl BotConfig { 58 | /// Generate a `BotConfig` from a TOML config file. 59 | /// 60 | /// If `path` matches `None`, will search for a file called `config.toml` in an XDG 61 | /// compliant configuration directory (e.g ~/.config/trinity/config.toml on Linux). 62 | pub fn from_config(path: Option) -> anyhow::Result { 63 | let config_path = match path { 64 | Some(a) => a, 65 | None => { 66 | let dirs = directories::ProjectDirs::from("", "", "trinity") 67 | .context("config file not found")?; 68 | let path = dirs.config_dir().join("config.toml"); 69 | String::from(path.to_str().unwrap()) 70 | } 71 | }; 72 | let contents = fs::read_to_string(&config_path)?; 73 | let config: BotConfig = toml::from_str(&contents)?; 74 | 75 | debug!("Using configuration from {config_path}"); 76 | Ok(config) 77 | } 78 | 79 | /// Generate a `BotConfig` from the process' environment. 80 | pub fn from_env() -> anyhow::Result { 81 | // override environment variables with contents of .env file, unless they were already set 82 | // explicitly. 83 | dotenvy::dotenv().ok(); 84 | 85 | let home_server = env::var("HOMESERVER").context("missing HOMESERVER variable")?; 86 | let user_id = env::var("BOT_USER_ID").context("missing bot user id in BOT_USER_ID")?; 87 | let password = env::var("BOT_PWD").context("missing bot user id in BOT_PWD")?; 88 | let matrix_store_path = 89 | env::var("MATRIX_STORE_PATH").context("missing MATRIX_STORE_PATH")?; 90 | let redb_path = env::var("REDB_PATH").context("missing REDB_PATH")?; 91 | 92 | let admin_user_id = 93 | env::var("ADMIN_USER_ID").context("missing admin user id in ADMIN_USER_ID")?; 94 | let admin_user_id = admin_user_id 95 | .try_into() 96 | .context("impossible to parse admin user id")?; 97 | 98 | // Read the module paths (separated by commas), check they exist, and return the whole 99 | // list. 100 | let modules_paths = env::var("MODULES_PATHS") 101 | .as_deref() 102 | .unwrap_or("./modules/target/wasm32-unknown-unknown/release") 103 | .split(',') 104 | .map(|path| { 105 | let path = PathBuf::from(path); 106 | anyhow::ensure!( 107 | path.exists(), 108 | "{} doesn't reference a valid path", 109 | path.to_string_lossy() 110 | ); 111 | Ok(path) 112 | }) 113 | .collect::>>() 114 | .context("a module path isn't valid")?; 115 | 116 | debug!("Using configuration from environment"); 117 | Ok(Self { 118 | home_server, 119 | user_id, 120 | password, 121 | matrix_store_path, 122 | admin_user_id, 123 | redb_path, 124 | modules_paths, 125 | modules_config: None, 126 | }) 127 | } 128 | } 129 | 130 | pub(crate) type ShareableDatabase = Arc; 131 | 132 | struct AppCtx { 133 | modules: WasmModules, 134 | modules_paths: Vec, 135 | modules_config: HashMap>, 136 | engine: wasmtime::Engine, 137 | needs_recompile: bool, 138 | admin_user_id: OwnedUserId, 139 | db: ShareableDatabase, 140 | room_resolver: RoomResolver, 141 | } 142 | 143 | impl AppCtx { 144 | /// Create a new `AppCtx`. 145 | pub fn new( 146 | client: Client, 147 | modules_paths: Vec, 148 | modules_config: HashMap>, 149 | db: ShareableDatabase, 150 | admin_user_id: OwnedUserId, 151 | ) -> anyhow::Result { 152 | let room_resolver = RoomResolver::new(client); 153 | 154 | let mut config = wasmtime::Config::new(); 155 | config.wasm_component_model(true); 156 | config.cache_config_load_default()?; 157 | 158 | let engine = wasmtime::Engine::new(&config)?; 159 | 160 | Ok(Self { 161 | modules: WasmModules::new(&engine, db.clone(), &modules_paths, &modules_config)?, 162 | modules_paths, 163 | modules_config, 164 | needs_recompile: false, 165 | admin_user_id, 166 | db, 167 | room_resolver, 168 | engine, 169 | }) 170 | } 171 | 172 | pub async fn set_needs_recompile(ptr: Arc>) { 173 | { 174 | let need = &mut ptr.lock().await.needs_recompile; 175 | if *need { 176 | return; 177 | } 178 | *need = true; 179 | } 180 | 181 | let mut ptr = futures::executor::block_on(async { 182 | tokio::time::sleep(Duration::new(1, 0)).await; 183 | ptr.lock().await 184 | }); 185 | 186 | match WasmModules::new( 187 | &ptr.engine, 188 | ptr.db.clone(), 189 | &ptr.modules_paths, 190 | &ptr.modules_config, 191 | ) { 192 | Ok(modules) => { 193 | ptr.modules = modules; 194 | info!("successful hot reload!"); 195 | } 196 | Err(err) => { 197 | error!("hot reload failed: {err:#}"); 198 | } 199 | } 200 | 201 | ptr.needs_recompile = false; 202 | } 203 | } 204 | 205 | #[derive(Clone)] 206 | struct App { 207 | inner: Arc>, 208 | } 209 | 210 | impl App { 211 | pub fn new(ctx: AppCtx) -> Self { 212 | Self { 213 | inner: Arc::new(Mutex::new(ctx)), 214 | } 215 | } 216 | } 217 | 218 | /// Try to handle a message assuming it's an `!admin` command. 219 | fn try_handle_admin<'a>( 220 | content: &str, 221 | sender: &UserId, 222 | room: &RoomId, 223 | modules: impl Iterator, 224 | room_resolver: &mut RoomResolver, 225 | ) -> Option> { 226 | let Some(rest) = content.strip_prefix("!admin") else { 227 | return None; 228 | }; 229 | 230 | trace!("trying admin for {content}"); 231 | 232 | if let Some(rest) = rest.strip_prefix(' ') { 233 | let rest = rest.trim(); 234 | if let Some((module, rest)) = rest.split_once(' ').map(|(l, r)| (l, r.trim())) { 235 | // If the next word resolves to a valid room id use that, otherwise use the 236 | // current room. 237 | let (possible_room, rest) = rest 238 | .split_once(' ') 239 | .map_or((rest, ""), |(l, r)| (l, r.trim())); 240 | 241 | let (target_room, rest) = match room_resolver.resolve_room(possible_room) { 242 | Ok(Some(resolved_room)) => (resolved_room, rest.to_string()), 243 | Ok(None) | Err(_) => (room.to_string(), format!("{} {}", possible_room, rest)), 244 | }; 245 | 246 | let mut found = None; 247 | for m in modules { 248 | if m.name() == module { 249 | found = match m.admin(rest.trim(), sender, target_room.as_str()) { 250 | Ok(actions) => Some(actions), 251 | Err(err) => { 252 | error!("error when handling admin command: {err:#}"); 253 | None 254 | } 255 | }; 256 | break; 257 | } 258 | } 259 | found 260 | } else { 261 | Some(vec![wasm::Action::Respond(wasm::Message { 262 | text: "missing command".to_owned(), 263 | html: None, 264 | to: sender.to_string(), 265 | })]) 266 | } 267 | } else { 268 | Some(vec![wasm::Action::Respond(wasm::Message { 269 | text: "missing module and command".to_owned(), 270 | html: None, 271 | to: sender.to_string(), 272 | })]) 273 | } 274 | } 275 | 276 | fn try_handle_help<'a>( 277 | content: &str, 278 | sender: &UserId, 279 | modules: impl Iterator, 280 | ) -> Option { 281 | let Some(rest) = content.strip_prefix("!help") else { 282 | return None; 283 | }; 284 | 285 | // Special handling for help messages. 286 | let (msg, html) = if rest.trim().is_empty() { 287 | let mut msg = String::from("Available modules:"); 288 | let mut html = String::from("Available modules:
    "); 289 | for m in modules { 290 | let help = match m.help(None) { 291 | Ok(msg) => Some(msg), 292 | Err(err) => { 293 | error!("error when handling help command: {err:#}"); 294 | None 295 | } 296 | } 297 | .unwrap_or("".to_string()); 298 | 299 | msg.push_str(&format!("\n- {name}: {help}", name = m.name(), help = help)); 300 | // TODO lol sanitize html 301 | html.push_str(&format!( 302 | "
  • {name}: {help}
  • ", 303 | name = m.name(), 304 | help = help 305 | )); 306 | } 307 | html.push_str("
"); 308 | 309 | (msg, html) 310 | } else if let Some(rest) = rest.strip_prefix(' ') { 311 | let rest = rest.trim(); 312 | let (module, topic) = rest 313 | .split_once(' ') 314 | .map(|(l, r)| (l, Some(r.trim()))) 315 | .unwrap_or((rest, None)); 316 | let mut found = None; 317 | for m in modules { 318 | if m.name() == module { 319 | found = m.help(topic).ok(); 320 | break; 321 | } 322 | } 323 | let msg = if let Some(content) = found { 324 | content 325 | } else { 326 | format!("module {module} not found") 327 | }; 328 | (msg.clone(), msg) 329 | } else { 330 | return None; 331 | }; 332 | 333 | return Some(wasm::Action::Respond(wasm::Message { 334 | text: msg, 335 | html: Some(html), 336 | to: sender.to_string(), // TODO rather room? 337 | })); 338 | } 339 | 340 | enum AnyEvent { 341 | RoomMessage(RoomMessageEventContent), 342 | Reaction(ReactionEventContent), 343 | } 344 | 345 | impl AnyEvent { 346 | async fn send(self, room: &mut matrix_sdk::room::Joined) -> anyhow::Result<()> { 347 | let _ = match self { 348 | AnyEvent::RoomMessage(e) => room.send(e, None).await?, 349 | AnyEvent::Reaction(e) => room.send(e, None).await?, 350 | }; 351 | Ok(()) 352 | } 353 | } 354 | 355 | async fn on_message( 356 | ev: SyncRoomMessageEvent, 357 | room: Room, 358 | client: Client, 359 | Ctx(ctx): Ctx, 360 | ) -> anyhow::Result<()> { 361 | let mut room = if let Room::Joined(room) = room { 362 | room 363 | } else { 364 | // Ignore non-joined rooms events. 365 | return Ok(()); 366 | }; 367 | 368 | if ev.sender() == client.user_id().unwrap() { 369 | // Skip messages sent by the bot. 370 | return Ok(()); 371 | } 372 | 373 | if let Some(unredacted) = ev.as_original() { 374 | let content = if let MessageType::Text(text) = &unredacted.content.msgtype { 375 | text.body.to_string() 376 | } else { 377 | // Ignore other kinds of messages at the moment. 378 | return Ok(()); 379 | }; 380 | 381 | trace!( 382 | "Received a message from {} in {}: {}", 383 | ev.sender(), 384 | room.room_id(), 385 | content, 386 | ); 387 | 388 | // TODO ohnoes, locking across other awaits is bad 389 | // TODO Use a lock-free data-structure for the list of modules + put locks in the module 390 | // internal implementation? 391 | // TODO or create a new wasm instance per message \o/ 392 | let ctx = ctx.inner.clone(); 393 | let room_id = room.room_id().to_owned(); 394 | 395 | let event_id = ev.event_id().to_owned(); 396 | 397 | let new_actions = tokio::task::spawn_blocking(move || { 398 | let ctx = &mut *futures::executor::block_on(ctx.lock()); 399 | 400 | if ev.sender() == ctx.admin_user_id { 401 | match try_handle_admin( 402 | &content, 403 | &ctx.admin_user_id, 404 | &room_id, 405 | ctx.modules.iter_mut(), 406 | &mut ctx.room_resolver, 407 | ) { 408 | None => {} 409 | Some(actions) => { 410 | trace!("handled by admin, skipping modules"); 411 | return actions; 412 | } 413 | } 414 | } 415 | 416 | if let Some(actions) = try_handle_help(&content, ev.sender(), ctx.modules.iter_mut()) { 417 | trace!("handled by help, skipping modules"); 418 | return vec![actions]; 419 | } 420 | 421 | for module in ctx.modules.iter_mut() { 422 | trace!("trying to handle message with {}...", module.name()); 423 | match module.handle(&content, ev.sender(), &room_id) { 424 | Ok(actions) => { 425 | if !actions.is_empty() { 426 | // TODO support handling the same message with several handlers. 427 | trace!("{} returned a response!", module.name()); 428 | return actions; 429 | } 430 | } 431 | Err(err) => { 432 | warn!("wasm module {} ran into an error: {err}", module.name()); 433 | } 434 | } 435 | } 436 | 437 | Vec::new() 438 | }) 439 | .await?; 440 | 441 | let new_events = new_actions 442 | .into_iter() 443 | .map(|a| match a { 444 | wasm::Action::Respond(msg) => { 445 | let content = if let Some(html) = msg.html { 446 | RoomMessageEventContent::text_html(msg.text, html) 447 | } else { 448 | RoomMessageEventContent::text_plain(msg.text) 449 | }; 450 | AnyEvent::RoomMessage(content) 451 | } 452 | wasm::Action::React(reaction) => { 453 | let reaction = 454 | ReactionEventContent::new(Relation::new(event_id.clone(), reaction)); 455 | AnyEvent::Reaction(reaction) 456 | } 457 | }) 458 | .collect::>(); 459 | 460 | for event in new_events { 461 | event.send(&mut room).await?; 462 | } 463 | } 464 | 465 | Ok(()) 466 | } 467 | 468 | /// Autojoin mixin. 469 | async fn on_stripped_state_member( 470 | room_member: StrippedRoomMemberEvent, 471 | client: Client, 472 | room: Room, 473 | ) { 474 | if room_member.state_key != client.user_id().unwrap() { 475 | // the invite we've seen isn't for us, but for someone else. ignore 476 | return; 477 | } 478 | 479 | // looks like the room is an invited room, let's attempt to join then 480 | if let Room::Invited(room) = room { 481 | // The event handlers are called before the next sync begins, but 482 | // methods that change the state of a room (joining, leaving a room) 483 | // wait for the sync to return the new room state so we need to spawn 484 | // a new task for them. 485 | tokio::spawn(async move { 486 | debug!("Autojoining room {}", room.room_id()); 487 | let mut delay = 1; 488 | 489 | while let Err(err) = room.accept_invitation().await { 490 | // retry autojoin due to synapse sending invites, before the 491 | // invited user can join for more information see 492 | // https://github.com/matrix-org/synapse/issues/4345 493 | warn!( 494 | "Failed to join room {} ({err:?}), retrying in {delay}s", 495 | room.room_id() 496 | ); 497 | 498 | sleep(Duration::from_secs(delay)).await; 499 | delay *= 2; 500 | 501 | if delay > 3600 { 502 | error!("Can't join room {} ({err:?})", room.room_id()); 503 | break; 504 | } 505 | } 506 | 507 | debug!("Successfully joined room {}", room.room_id()); 508 | }); 509 | } 510 | } 511 | 512 | /// Run the client for the given `BotConfig`. 513 | pub async fn run(config: BotConfig) -> anyhow::Result<()> { 514 | let client = Client::builder() 515 | .server_name(config.home_server.as_str().try_into()?) 516 | .sled_store(&config.matrix_store_path, None)? 517 | .build() 518 | .await?; 519 | 520 | // Create the database, and try to find a device id. 521 | let db = Arc::new(unsafe { redb::Database::create(config.redb_path, 1024 * 1024)? }); 522 | 523 | // First we need to log in. 524 | debug!("logging in..."); 525 | let mut login_builder = client.login_username(&config.user_id, &config.password); 526 | 527 | let mut db_device_id = None; 528 | if let Some(device_id) = admin_table::read_str(&db, DEVICE_ID_ENTRY) 529 | .context("reading device_id from the database")? 530 | { 531 | trace!("reusing previous device_id..."); 532 | // the login builder keeps a reference to the previous device id string, so can't clone 533 | // db_device_id here, it has to outlive the login_builder. 534 | db_device_id = Some(device_id); 535 | login_builder = login_builder.device_id(db_device_id.as_ref().unwrap()); 536 | } 537 | 538 | let resp = login_builder.send().await?; 539 | 540 | let resp_device_id = resp.device_id.to_string(); 541 | if db_device_id.as_ref() != Some(&resp_device_id) { 542 | match db_device_id { 543 | Some(prev) => { 544 | warn!("overriding device_id (previous was {prev}, new is {resp_device_id})") 545 | } 546 | None => debug!("storing new device_id for the first time..."), 547 | } 548 | admin_table::write_str(&db, DEVICE_ID_ENTRY, &resp_device_id) 549 | .context("writing new device_id into the database")?; 550 | } 551 | 552 | let modules_config = config.modules_config.unwrap_or_else(HashMap::new); 553 | 554 | client 555 | .user_id() 556 | .context("impossible state: missing user id for the logged in bot?")?; 557 | 558 | // An initial sync to set up state and so our bot doesn't respond to old 559 | // messages. If the `StateStore` finds saved state in the location given the 560 | // initial sync will be skipped in favor of loading state from the store 561 | debug!("starting initial sync..."); 562 | client.sync_once(SyncSettings::default()).await.unwrap(); 563 | 564 | debug!("setting up app..."); 565 | let client_copy = client.clone(); 566 | let app_ctx = AppCtx::new( 567 | client_copy, 568 | config.modules_paths, 569 | modules_config, 570 | db, 571 | config.admin_user_id, 572 | )?; 573 | let app = App::new(app_ctx); 574 | 575 | let _watcher_guard = watcher(app.inner.clone()).await?; 576 | 577 | debug!("setup ready! now listening to incoming messages."); 578 | client.add_event_handler_context(app); 579 | client.add_event_handler(on_message); 580 | client.add_event_handler(on_stripped_state_member); 581 | 582 | // Note: this method will never return. 583 | let sync_settings = SyncSettings::default().token(client.sync_token().await.unwrap()); 584 | 585 | tokio::select! { 586 | _ = handle_signals() => { 587 | // Exit :) 588 | } 589 | 590 | Err(err) = client.sync(sync_settings) => { 591 | anyhow::bail!(err); 592 | } 593 | } 594 | 595 | // Set bot presence to offline. 596 | let request = matrix_sdk::ruma::api::client::presence::set_presence::v3::Request::new( 597 | client.user_id().unwrap(), 598 | PresenceState::Offline, 599 | ); 600 | 601 | client.send(request, None).await?; 602 | 603 | info!("properly exited, have a nice day!"); 604 | Ok(()) 605 | } 606 | 607 | async fn handle_signals() -> anyhow::Result<()> { 608 | use futures::StreamExt as _; 609 | use signal_hook::consts::signal::*; 610 | use signal_hook_tokio::*; 611 | 612 | let mut signals = Signals::new(&[SIGINT, SIGHUP, SIGQUIT, SIGTERM])?; 613 | let handle = signals.handle(); 614 | 615 | while let Some(signal) = signals.next().await { 616 | match signal { 617 | SIGINT | SIGHUP | SIGQUIT | SIGTERM => { 618 | handle.close(); 619 | break; 620 | } 621 | _ => { 622 | // Don't care. 623 | } 624 | } 625 | } 626 | 627 | Ok(()) 628 | } 629 | 630 | async fn watcher(app: Arc>) -> anyhow::Result> { 631 | let modules_paths = { app.lock().await.modules_paths.clone() }; 632 | 633 | let mut watchers = Vec::with_capacity(modules_paths.len()); 634 | for modules_path in modules_paths { 635 | debug!( 636 | "setting up watcher on @ {}...", 637 | modules_path.to_string_lossy() 638 | ); 639 | 640 | let rt_handle = tokio::runtime::Handle::current(); 641 | let app = app.clone(); 642 | let mut watcher = notify::recommended_watcher( 643 | move |res: Result| match res { 644 | Ok(event) => { 645 | // Only watch wasm files 646 | if !event.paths.iter().any(|path| { 647 | if let Some(ext) = path.extension() { 648 | ext == "wasm" 649 | } else { 650 | false 651 | } 652 | }) { 653 | return; 654 | } 655 | 656 | match event.kind { 657 | notify::EventKind::Create(_) 658 | | notify::EventKind::Modify(_) 659 | | notify::EventKind::Remove(_) => { 660 | // Trigger an update of the modules. 661 | let app = app.clone(); 662 | rt_handle.spawn(async move { 663 | AppCtx::set_needs_recompile(app).await; 664 | }); 665 | } 666 | notify::EventKind::Access(_) 667 | | notify::EventKind::Any 668 | | notify::EventKind::Other => {} 669 | } 670 | } 671 | Err(e) => warn!("watch error: {e:?}"), 672 | }, 673 | )?; 674 | 675 | watcher.watch(&modules_path, RecursiveMode::Recursive)?; 676 | watchers.push(watcher); 677 | } 678 | 679 | debug!("watcher setup done!"); 680 | Ok(watchers) 681 | } 682 | -------------------------------------------------------------------------------- /src/room_resolver.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use matrix_sdk::{ 4 | ruma::{OwnedRoomAliasId, OwnedRoomId}, 5 | Client, 6 | }; 7 | 8 | pub(super) struct RoomResolver { 9 | client: Client, 10 | /// In-memory cache for the room alias to room id mapping. 11 | room_cache: HashMap, 12 | } 13 | 14 | impl RoomResolver { 15 | pub fn new(client: Client) -> Self { 16 | Self { 17 | client, 18 | room_cache: Default::default(), 19 | } 20 | } 21 | 22 | pub fn resolve_room(&mut self, room: &str) -> anyhow::Result> { 23 | if !room.starts_with('#') && !room.starts_with('!') { 24 | // This is likely not meant to be a room. 25 | return Ok(None); 26 | } 27 | 28 | // Shortcut: if the room is already a room id, return it. 29 | if let Ok(room_id) = OwnedRoomId::try_from(room) { 30 | return Ok(Some(room_id.to_string())); 31 | }; 32 | 33 | // Try to resolve the room alias; if it's not valid, we report an error to the caller here. 34 | let room_alias = OwnedRoomAliasId::try_from(room)?; 35 | 36 | // Try cache first... 37 | if let Some(cached) = self.room_cache.get(&room_alias) { 38 | return Ok(Some(cached.to_string())); 39 | } 40 | 41 | // ...but if it fails, sync query the server. 42 | let client = self.client.clone(); 43 | let room_alias_copy = room_alias.clone(); 44 | let result = futures::executor::block_on(async move { 45 | client.resolve_room_alias(&room_alias_copy).await 46 | })?; 47 | 48 | let room_id = result.room_id; 49 | self.room_cache.insert(room_alias, room_id.clone()); 50 | Ok(Some(room_id.to_string())) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/wasm.rs: -------------------------------------------------------------------------------- 1 | mod module { 2 | wasmtime::component::bindgen!({ 3 | path: "./wit/trinity-module.wit", 4 | }); 5 | } 6 | 7 | use crate::wasm::module::exports::trinity::module::messaging; 8 | pub(crate) use messaging::Action; 9 | pub(crate) use messaging::Message; 10 | use module::TrinityModule; 11 | use rayon::iter::IntoParallelIterator as _; 12 | use rayon::iter::ParallelIterator as _; 13 | use wasmtime::Store; 14 | 15 | mod apis; 16 | 17 | use std::collections::HashMap; 18 | use std::path::PathBuf; 19 | 20 | use matrix_sdk::ruma::{RoomId, UserId}; 21 | 22 | use crate::{wasm::apis::Apis, ShareableDatabase}; 23 | 24 | pub struct ModuleState { 25 | apis: Apis, 26 | } 27 | 28 | pub(crate) struct Module { 29 | name: String, 30 | instance: TrinityModule, 31 | pub store: Store, 32 | } 33 | 34 | impl Module { 35 | pub fn name(&self) -> &str { 36 | self.name.as_str() 37 | } 38 | 39 | pub fn help(&mut self, topic: Option<&str>) -> anyhow::Result { 40 | self.instance 41 | .trinity_module_messaging() 42 | .call_help(&mut self.store, topic) 43 | } 44 | 45 | pub fn admin( 46 | &mut self, 47 | cmd: &str, 48 | sender: &UserId, 49 | room: &str, 50 | ) -> anyhow::Result> { 51 | self.instance.trinity_module_messaging().call_admin( 52 | &mut self.store, 53 | cmd, 54 | sender.as_str(), 55 | room, 56 | ) 57 | } 58 | 59 | pub fn handle( 60 | &mut self, 61 | content: &str, 62 | sender: &UserId, 63 | room: &RoomId, 64 | ) -> anyhow::Result> { 65 | self.instance.trinity_module_messaging().call_on_msg( 66 | &mut self.store, 67 | content, 68 | sender.as_str(), 69 | "author name NYI", 70 | room.as_str(), 71 | ) 72 | } 73 | } 74 | 75 | #[derive(Default)] 76 | pub(crate) struct WasmModules { 77 | modules: Vec, 78 | } 79 | 80 | impl WasmModules { 81 | /// Create a new collection of wasm modules. 82 | pub fn new( 83 | engine: &wasmtime::Engine, 84 | db: ShareableDatabase, 85 | modules_paths: &[PathBuf], 86 | modules_config: &HashMap>, 87 | ) -> anyhow::Result { 88 | tracing::debug!("setting up wasm context..."); 89 | 90 | let mut compiled_modules = Vec::new(); 91 | 92 | tracing::debug!("precompiling wasm modules..."); 93 | for modules_path in modules_paths { 94 | tracing::debug!( 95 | "looking for modules in {}...", 96 | modules_path.to_string_lossy() 97 | ); 98 | 99 | // Collect all the modules paths and names. 100 | let mut path_and_names = vec![]; 101 | for module_path in std::fs::read_dir(modules_path)? { 102 | let module_path = module_path?.path(); 103 | 104 | if module_path.extension().map_or(true, |ext| ext != "wasm") { 105 | continue; 106 | } 107 | 108 | let name = module_path 109 | .file_stem() 110 | .map(|s| s.to_string_lossy()) 111 | .unwrap_or_else(|| module_path.to_string_lossy()) 112 | .to_string(); 113 | 114 | path_and_names.push((module_path, name)); 115 | } 116 | 117 | // Compile and re-init all the modules in parallel. 118 | let batch: Vec<_> = path_and_names 119 | .into_par_iter() 120 | .map(|(module_path, name)| -> anyhow::Result { 121 | let span = tracing::debug_span!("compiling module", name = %name, ); 122 | let _scope = span.enter(); 123 | 124 | tracing::debug!( 125 | path = module_path.to_str().unwrap_or(""), 126 | "initializing: creating APIs" 127 | ); 128 | let module_state = ModuleState { 129 | apis: Apis::new(name.clone(), db.clone())?, 130 | }; 131 | 132 | let mut store = wasmtime::Store::new(&engine, module_state); 133 | let mut linker = wasmtime::component::Linker::new(&engine); 134 | 135 | apis::Apis::link(&mut linker)?; 136 | 137 | tracing::debug!("compiling"); 138 | let component = 139 | wasmtime::component::Component::from_file(&engine, &module_path)?; 140 | 141 | tracing::debug!("instantiating"); 142 | let instance = 143 | module::TrinityModule::instantiate(&mut store, &component, &linker)?; 144 | 145 | // Convert the module config to Vec of tuples to satisfy wasm interface types. 146 | let init_config: Option> = modules_config 147 | .get(&name) 148 | .map(|mc| Vec::from_iter(mc.clone())); 149 | 150 | tracing::debug!("calling module's init() function"); 151 | instance 152 | .trinity_module_messaging() 153 | .call_init(&mut store, init_config.as_deref())?; 154 | 155 | tracing::debug!("great success!"); 156 | Ok(Module { 157 | name, 158 | instance, 159 | store, 160 | }) 161 | }) 162 | .collect::, _>>()?; 163 | 164 | compiled_modules.extend(batch); 165 | } 166 | 167 | Ok(Self { 168 | modules: compiled_modules, 169 | }) 170 | } 171 | 172 | pub(crate) fn iter_mut(&mut self) -> impl Iterator { 173 | self.modules.iter_mut() 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/wasm/apis/kv_store.rs: -------------------------------------------------------------------------------- 1 | use redb::{ReadableTable as _, TableDefinition}; 2 | 3 | use crate::wasm::apis::kv_store::trinity::api::kv; 4 | use crate::wasm::ModuleState; 5 | use crate::ShareableDatabase; 6 | 7 | wasmtime::component::bindgen!({ 8 | path: "./wit/kv.wit", 9 | world: "kv-world" 10 | }); 11 | 12 | pub(super) struct KeyValueStoreApi { 13 | db: ShareableDatabase, 14 | module_name: String, 15 | } 16 | 17 | impl KeyValueStoreApi { 18 | pub fn new(db: ShareableDatabase, module_name: &str) -> anyhow::Result { 19 | Ok(Self { 20 | db, 21 | module_name: module_name.to_owned(), 22 | }) 23 | } 24 | 25 | pub fn link(linker: &mut wasmtime::component::Linker) -> anyhow::Result<()> { 26 | kv::add_to_linker(linker, move |s| &mut s.apis.kv_store) 27 | } 28 | 29 | fn set_impl(&mut self, key: Vec, value: Vec) -> anyhow::Result<()> { 30 | let table_def = TableDefinition::<[u8], [u8]>::new(&self.module_name); 31 | let txn = self.db.begin_write()?; 32 | { 33 | let mut table = txn.open_table(table_def)?; 34 | table.insert(&key, &value)?; 35 | } 36 | txn.commit()?; 37 | Ok(()) 38 | } 39 | 40 | fn get_impl(&mut self, key: Vec) -> anyhow::Result>> { 41 | let table_def = TableDefinition::<[u8], [u8]>::new(&self.module_name); 42 | let txn = self.db.begin_read()?; 43 | let table = match txn.open_table(table_def) { 44 | Ok(table) => table, 45 | Err(err) => match err { 46 | redb::Error::DatabaseAlreadyOpen 47 | | redb::Error::InvalidSavepoint 48 | | redb::Error::Corrupted(_) 49 | | redb::Error::TableTypeMismatch(_) 50 | | redb::Error::DbSizeMismatch { .. } 51 | | redb::Error::TableAlreadyOpen(_, _) 52 | | redb::Error::OutOfSpace 53 | | redb::Error::Io(_) 54 | | redb::Error::LockPoisoned(_) => Err(err)?, 55 | redb::Error::TableDoesNotExist(_) => return Ok(None), 56 | }, 57 | }; 58 | Ok(table.get(&key)?.map(|val| val.to_vec())) 59 | } 60 | 61 | fn remove_impl(&mut self, key: Vec) -> anyhow::Result<()> { 62 | let table_def = TableDefinition::<[u8], [u8]>::new(&self.module_name); 63 | let txn = self.db.begin_write()?; 64 | { 65 | let mut table = txn.open_table(table_def)?; 66 | table.remove(&key)?; 67 | } 68 | txn.commit()?; 69 | Ok(()) 70 | } 71 | } 72 | 73 | impl kv::Host for KeyValueStoreApi { 74 | fn set(&mut self, key: Vec, value: Vec) -> Result<(), kv::KvError> { 75 | self.set_impl(key, value) 76 | .map_err(|err: anyhow::Error| kv::KvError::Internal(err.to_string())) 77 | } 78 | 79 | fn get(&mut self, key: Vec) -> Result>, kv::KvError> { 80 | self.get_impl(key) 81 | .map_err(|err: anyhow::Error| kv::KvError::Internal(err.to_string())) 82 | } 83 | 84 | fn remove(&mut self, key: Vec) -> Result<(), kv::KvError> { 85 | self.remove_impl(key) 86 | .map_err(|err: anyhow::Error| kv::KvError::Internal(err.to_string())) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/wasm/apis/log.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm::apis::log::trinity::api::log; 2 | use crate::wasm::ModuleState; 3 | 4 | wasmtime::component::bindgen!({ 5 | path: "./wit/log.wit", 6 | world: "log-world" 7 | }); 8 | 9 | pub(super) struct LogApi { 10 | module_name: String, 11 | } 12 | 13 | impl LogApi { 14 | pub fn new(module_name: &str) -> Self { 15 | Self { 16 | module_name: module_name.to_owned(), 17 | } 18 | } 19 | 20 | pub fn link(linker: &mut wasmtime::component::Linker) -> wasmtime::Result<()> { 21 | log::add_to_linker(linker, move |s| &mut s.apis.log) 22 | } 23 | } 24 | 25 | impl log::Host for LogApi { 26 | fn trace(&mut self, msg: String) { 27 | tracing::trace!("{} - {msg}", self.module_name); 28 | } 29 | fn debug(&mut self, msg: String) { 30 | tracing::debug!("{} - {msg}", self.module_name); 31 | } 32 | fn info(&mut self, msg: String) { 33 | tracing::info!("{} - {msg}", self.module_name); 34 | } 35 | fn warn(&mut self, msg: String) { 36 | tracing::warn!("{} - {msg}", self.module_name); 37 | } 38 | fn error(&mut self, msg: String) { 39 | tracing::error!("{} - {msg}", self.module_name); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/wasm/apis/mod.rs: -------------------------------------------------------------------------------- 1 | mod kv_store; 2 | mod log; 3 | mod sync_request; 4 | mod sys; 5 | 6 | use crate::ShareableDatabase; 7 | 8 | use self::kv_store::KeyValueStoreApi; 9 | use self::log::LogApi; 10 | use self::sync_request::SyncRequestApi; 11 | use self::sys::SysApi; 12 | 13 | use super::ModuleState; 14 | 15 | pub(crate) struct Apis { 16 | sys: SysApi, 17 | log: LogApi, 18 | sync_request: SyncRequestApi, 19 | kv_store: KeyValueStoreApi, 20 | } 21 | 22 | impl Apis { 23 | pub fn new(module_name: String, db: ShareableDatabase) -> anyhow::Result { 24 | Ok(Self { 25 | sys: SysApi {}, 26 | log: LogApi::new(&module_name), 27 | sync_request: SyncRequestApi::default(), 28 | kv_store: KeyValueStoreApi::new(db, &module_name)?, 29 | }) 30 | } 31 | 32 | pub fn link(linker: &mut wasmtime::component::Linker) -> anyhow::Result<()> { 33 | sys::SysApi::link(linker)?; 34 | log::LogApi::link(linker)?; 35 | sync_request::SyncRequestApi::link(linker)?; 36 | kv_store::KeyValueStoreApi::link(linker)?; 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/wasm/apis/sync_request.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm::apis::sync_request::trinity::api::sync_request; 2 | use crate::wasm::ModuleState; 3 | 4 | wasmtime::component::bindgen!({ 5 | path: "./wit/sync-request.wit", 6 | world: "sync-request-world" 7 | }); 8 | 9 | use sync_request::*; 10 | 11 | #[derive(Default)] 12 | pub(super) struct SyncRequestApi { 13 | client: reqwest::blocking::Client, 14 | } 15 | 16 | impl SyncRequestApi { 17 | pub fn link(linker: &mut wasmtime::component::Linker) -> anyhow::Result<()> { 18 | sync_request::add_to_linker(linker, move |s| &mut s.apis.sync_request) 19 | } 20 | } 21 | 22 | impl sync_request::Host for SyncRequestApi { 23 | fn run_request(&mut self, req: Request) -> Result { 24 | let url = req.url; 25 | let mut builder = match req.verb { 26 | RequestVerb::Get => self.client.get(url), 27 | RequestVerb::Put => self.client.put(url), 28 | RequestVerb::Delete => self.client.delete(url), 29 | RequestVerb::Post => self.client.post(url), 30 | }; 31 | for header in req.headers { 32 | builder = builder.header(header.key, header.value); 33 | } 34 | if let Some(body) = req.body { 35 | builder = builder.body(body); 36 | } 37 | let req = builder 38 | .build() 39 | .map_err(|err| RunRequestError::Builder(err.to_string()))?; 40 | 41 | let resp = self 42 | .client 43 | .execute(req) 44 | .map_err(|err| RunRequestError::Execute(err.to_string()))?; 45 | 46 | let status = match resp.status().as_u16() / 100 { 47 | 2 => ResponseStatus::Success, 48 | _ => ResponseStatus::Error, 49 | }; 50 | 51 | let body = resp.text().ok(); 52 | 53 | Ok(Response { status, body }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/wasm/apis/sys.rs: -------------------------------------------------------------------------------- 1 | use crate::wasm::apis::sys::trinity::api::sys; 2 | use crate::wasm::ModuleState; 3 | 4 | wasmtime::component::bindgen!({ 5 | path: "./wit/sys.wit", 6 | world: "sys-world" 7 | }); 8 | 9 | pub(super) struct SysApi; 10 | 11 | impl SysApi { 12 | pub fn link(linker: &mut wasmtime::component::Linker) -> wasmtime::Result<()> { 13 | sys::add_to_linker(linker, move |s| &mut s.apis.sys) 14 | } 15 | } 16 | 17 | impl sys::Host for SysApi { 18 | fn rand_u64(&mut self) -> u64 { 19 | rand::random() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /wit/kv.wit: -------------------------------------------------------------------------------- 1 | package trinity:api; 2 | 3 | interface kv { 4 | variant kv-error { 5 | internal(string) 6 | } 7 | 8 | set: func(key: list, value: list) -> result<_, kv-error>; 9 | get: func(key: list) -> result>, kv-error>; 10 | remove: func(key: list) -> result<_, kv-error>; 11 | } 12 | 13 | world kv-world { 14 | import kv; 15 | } 16 | -------------------------------------------------------------------------------- /wit/log.wit: -------------------------------------------------------------------------------- 1 | package trinity:api; 2 | 3 | interface log { 4 | trace: func(s: string); 5 | debug: func(s: string); 6 | info: func(s: string); 7 | warn: func(s: string); 8 | error: func(s: string); 9 | } 10 | 11 | world log-world { 12 | import log; 13 | } 14 | -------------------------------------------------------------------------------- /wit/sync-request.wit: -------------------------------------------------------------------------------- 1 | package trinity:api; 2 | 3 | interface sync-request { 4 | enum request-verb { 5 | get, put, delete, post 6 | } 7 | 8 | record request-header { 9 | key: string, 10 | value: string, 11 | } 12 | 13 | record request { 14 | verb: request-verb, 15 | url: string, 16 | headers: list, 17 | body: option 18 | } 19 | 20 | enum response-status { 21 | success, error 22 | } 23 | 24 | record response { 25 | status: response-status, 26 | body: option, 27 | } 28 | 29 | /// An error happened while trying to run a request. 30 | variant run-request-error { 31 | /// The builder couldn't be created. 32 | builder(string), 33 | /// The request couldn't be executed. 34 | execute(string) 35 | } 36 | 37 | run-request: func(req: request) -> result; 38 | } 39 | 40 | world sync-request-world { 41 | import sync-request; 42 | } 43 | -------------------------------------------------------------------------------- /wit/sys.wit: -------------------------------------------------------------------------------- 1 | package trinity:api; 2 | 3 | interface sys { 4 | rand-u64: func() -> u64; 5 | } 6 | 7 | world sys-world { 8 | import sys; 9 | } 10 | -------------------------------------------------------------------------------- /wit/trinity-module.wit: -------------------------------------------------------------------------------- 1 | package trinity:module; 2 | 3 | interface messaging { 4 | record message { 5 | text: string, 6 | html: option, 7 | to: string 8 | } 9 | 10 | type reaction = string; 11 | 12 | variant action { 13 | respond(message), 14 | react(reaction) 15 | } 16 | 17 | init: func(config: option>>); 18 | help: func(topic: option) -> string; 19 | admin: func(cmd: string, author-id: string, room: string) -> list; 20 | on-msg: func(content: string, author-id: string, author-name: string, room: string) -> list; 21 | } 22 | 23 | world trinity-module { 24 | export messaging; 25 | } 26 | --------------------------------------------------------------------------------