From b0f43c4bca25443404f6eaad079b40685e41395d Mon Sep 17 00:00:00 2001 From: Tim Hillier Date: Sat, 22 Nov 2025 10:40:18 -0800 Subject: [PATCH 1/3] Action to check cs and fmt of project. --- .github/workflows/rust-cs-fmt.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/rust-cs-fmt.yml diff --git a/.github/workflows/rust-cs-fmt.yml b/.github/workflows/rust-cs-fmt.yml new file mode 100644 index 0000000..737d9dd --- /dev/null +++ b/.github/workflows/rust-cs-fmt.yml @@ -0,0 +1,24 @@ +name: Rust Code Style and Format Check + +on: + pull_request: + +jobs: + rustfmt_clippy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Run rustfmt + run: cargo fmt --all -- --check + + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings From b9372d251e445118c59c5d12a75dc3130136b559 Mon Sep 17 00:00:00 2001 From: Tim Hillier Date: Sun, 30 Nov 2025 00:03:08 -0800 Subject: [PATCH 2/3] Runescape integration & GH Actiosn Update (#9) * item_count is now just count. Added help text about alaises. * fixed command call in main. * Basic Runscape GE item command. * Now pushes latest, and version branches. * RS GE integration. * Bump version to 4.5 --------- Co-authored-by: TIm Hillier --- .github/workflows/docker-build-push.yml | 17 +- Cargo.lock | 435 +++++++++++++++++++++++- Cargo.toml | 18 +- run.sh | 95 ------ src/commands/help.rs | 5 +- src/commands/mod.rs | 9 +- src/commands/runescape.rs | 204 +++++++++++ src/commands/shop.rs | 4 +- src/main.rs | 140 +++++--- src/runescape_utils/lib.rs | 19 ++ src/runescape_utils/mod.rs | 1 + src/runescape_utils/rs_client.rs | 159 +++++++++ 12 files changed, 943 insertions(+), 163 deletions(-) delete mode 100755 run.sh create mode 100644 src/commands/runescape.rs create mode 100644 src/runescape_utils/lib.rs create mode 100644 src/runescape_utils/mod.rs create mode 100644 src/runescape_utils/rs_client.rs diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index c5990cf..f768bff 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -17,8 +17,17 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build Docker image - run: docker build . -t ghcr.io/timhillier/rustbot:latest + - name: Extract version from Cargo.toml + id: get_version + run: | + version=$(grep '^version =' Cargo.toml | head -1 | sed -E "s/version = \"(.*)\"/\1/") + echo "version=$version" >> $GITHUB_OUTPUT - - name: Push Docker image - run: docker push ghcr.io/timhillier/rustbot:latest \ No newline at end of file + - name: Build Docker image with both tags + run: | + docker build . -t ghcr.io/timhillier/rustbot:latest -t ghcr.io/timhillier/rustbot:${{ steps.get_version.outputs.version }} + + - name: Push Docker images + run: | + docker push ghcr.io/timhillier/rustbot:latest + docker push ghcr.io/timhillier/rustbot:${{ steps.get_version.outputs.version }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3ac1fd4..bf11613 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,15 +4,23 @@ version = 4 [[package]] name = "RustBot" -version = "0.4.0" +version = "0.4.2" dependencies = [ + "chrono", "poise", + "quickchart-rs", "rand 0.9.2", + "reqwest 0.11.27", "serde", + "serde_json", "serenity", "sqlx", + "thiserror 1.0.69", + "thousands", "tokio", "toml", + "url", + "urlencoding", ] [[package]] @@ -74,6 +82,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -200,8 +214,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -540,6 +556,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -715,6 +746,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -819,6 +869,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -841,9 +914,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", - "http-body", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -855,6 +928,28 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -863,12 +958,83 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper", + "hyper 0.14.32", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.35", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -1017,6 +1183,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1177,6 +1353,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "num-bigint-dig" version = "0.8.5" @@ -1235,6 +1428,50 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking" version = "2.2.1" @@ -1391,6 +1628,18 @@ dependencies = [ "unicase", ] +[[package]] +name = "quickchart-rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ceeeda9f74729402f9df088b145d89f638d9d506eb6c2b815b81ce9e12140b" +dependencies = [ + "reqwest 0.12.24", + "serde_json", + "thiserror 2.0.17", + "url", +] + [[package]] name = "quote" version = "1.0.41" @@ -1514,16 +1763,18 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", - "http-body", - "hyper", - "hyper-rustls", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-rustls 0.24.2", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", "mime", "mime_guess", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -1532,9 +1783,10 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls 0.24.1", "tokio-util", "tower-service", @@ -1547,6 +1799,46 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-tls 0.6.0", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -1705,6 +1997,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1731,6 +2032,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -1835,7 +2159,7 @@ dependencies = [ "mime_guess", "parking_lot", "percent-encoding", - "reqwest", + "reqwest 0.11.27", "secrecy", "serde", "serde_cow", @@ -2217,6 +2541,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2308,6 +2641,12 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + [[package]] name = "time" version = "0.3.44" @@ -2390,6 +2729,16 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -2411,6 +2760,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.35", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -2490,6 +2849,45 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -2653,6 +3051,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -2888,6 +3292,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 0e02d77..0bd610c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,22 @@ [package] name = "RustBot" -version = "0.4.0" +version = "0.4.5" authors = ["Tim Hillier tim.r.hillier@gmail.com"] edition = "2024" [dependencies] -serenity = { version = "0.12.4", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache", "standard_framework", "framework", "utils", "voice"]} +serenity = { version = "0.12.4", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache", "standard_framework", "framework", "utils", "voice"] } tokio = { version = "1.32.0", features = ["macros", "rt-multi-thread"] } -serde = {version = "1.0.188", features = ["derive"]} +serde = { version = "1.0.188", features = ["derive"] } +serde_json = "1.0.145" toml = "0.9.8" rand = "0.9.2" -sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "tls-rustls", "sqlite"] } -poise = "0.6.1" \ No newline at end of file +sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "tls-rustls", "sqlite"] } +poise = "0.6.1" +thousands = "0.2.0" +quickchart-rs = { version = "0.1.1" } +chrono = "0.4.42" +reqwest = "0.11.27" +thiserror = "1.0.69" +url = "2.5.7" +urlencoding = "2.1.3" diff --git a/run.sh b/run.sh deleted file mode 100755 index 5535808..0000000 --- a/run.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash - -# RUN -# docker run --name rustbot -v $(pwd)/data:/app/data -d $image - -# to run without build -# ./run - -# to run with build ( use when new features get added ) -# ./run --build - -# to run with migration ( use when database changes are added ) -# ./run --migration - -# to run with migration and build -# ./run --build --migration - - -#default variables -build=false -migrate=false -remove=false -start=false - -#Help function -usage() { - echo "Usage: $0 [OPTIONS]" - echo "Options:" - echo " -h, --help Display this help message" - echo " -b, --build Builds the docker container" - echo " -m, --migration Runs Migrations." - echo " -r, --remove Removes the Old container prior to building" - echo " -s, --start Start the Container" -} - -handle_options() { - while [ $# -gt 0 ]; do - case $1 in - # display script help - -h | --help) - useage - exit 0 - ;; - # run docker with build. - -b | --build) - build=true - ;; - -r | --remove) - remove=true - ;; - # run docker with migration. - -m | --migration) - migrate=true - ;; - # start the docker container. - -s | --start) - start=true - ;; - *) - echo "Invalid option: $1" >&2 - usage - exit 1 - ;; - esac - shift - done -} - -# main script -handle_options "$@" - -# build the docker container. -if [ "$build" = true ]; then - echo "Building Docker Container" - if [ "$remove" = true ]; then - image_sha=$(docker images --no-trunc --quiet rustbot) - image_sha=${image_sha:7} - echo "Removing old Container: " $image_sha - docker image rm $image_sha - fi - docker build -t rustbot . -fi - - -image_sha=$(docker images --no-trunc --quiet rustbot) -image_sha=${image_sha:7} - -echo "Running Container: " $image_sha -docker run --name rustbot -v $(pwd)/data:/app/data -d $image_sha - -if [ "$migrate" = true ]; then - echo "Running Migrations" - # I need to be inside the container. - docker exec rustbot cargo sqlx migrate run --database-url sqlite:data/rustbot.sqlite --source data/migrations -fi \ No newline at end of file diff --git a/src/commands/help.rs b/src/commands/help.rs index cd5d119..4b49bea 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -16,8 +16,9 @@ pub async fn help ( }; } - let extra_text_at_bottom = "\ - Type `!help for more info."; + let extra_text_at_bottom = " + Type `!help for more info.\ + Some commands have aliases"; let config = HelpConfiguration { show_subcommands: true, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 2191fe9..a0da376 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,7 +1,8 @@ -pub mod ping; -pub mod smash; +pub mod help; pub mod judge; +pub mod ping; +pub mod runescape; pub mod score; -pub mod trade; pub mod shop; -pub mod help; \ No newline at end of file +pub mod smash; +pub mod trade; diff --git a/src/commands/runescape.rs b/src/commands/runescape.rs new file mode 100644 index 0000000..c80f6f3 --- /dev/null +++ b/src/commands/runescape.rs @@ -0,0 +1,204 @@ +use crate::bot_types::{_Context as Context, Error}; +use crate::runescape_utils::rs_client::{RSClient, RSPrice, TimeStampValue}; +use chrono::DateTime; +use poise::serenity_prelude as serenity; +use quickchart_rs::QuickchartClient; +use serde_json::json; +use thousands::Separable; + +// TODO: Move the image and embed stuff into its own method. :3 +/// Checks the Grand Exchange for the price of an item. +#[poise::command(prefix_command, aliases("price", "ge", "rsge", "rsprice"))] +pub async fn grand_exchange( + ctx: Context<'_>, + #[description = "The name of the item you want to look up"] + #[rest] + item: String, +) -> Result<(), Error> { + let response = RSClient::new().item_name(item).get_price().await?; + let item_name_formatted = response + .item + .replace(' ', "_") + .replace("'", "") + .replace("-", "_"); + + let image_url = format!( + "https://oldschool.runescape.wiki/images/{}.png", + item_name_formatted + ); + + let embed = serenity::CreateEmbed::default() + .title("💰 Grand Exchange Price") + .description(format!("**{}**", response.item)) + .field( + "💵 Price", + format!("{} gp", response.price.separate_with_commas()), + true, + ) + .field("📊 Volume", response.volume.separate_with_commas(), true) + .color(0xffd700) + .footer(serenity::CreateEmbedFooter::new( + "Old School RuneScape Grand Exchange", + )) + .thumbnail(image_url); + + ctx.send(poise::CreateReply::default().embed(embed)).await?; + + Ok(()) +} + +/// Checks the Grand Exchange history of an item for the past 10 days. +#[poise::command(prefix_command, aliases("priceHistory", "ph", "history", "hst"))] +pub async fn grand_exchange_history( + ctx: Context<'_>, + #[description = "The name of the item you want to look up"] + #[rest] + item: String, // make this optional so that if they just do !hs then it just does the last item. +) -> Result<(), Error> { + let time_length = 10; + let response = RSClient::new().item_name(item).get_price_history().await?; + let item_name_formatted = response + .item + .replace(' ', "_") + .replace("'", "") + .replace("-", "_"); + + let image_url = format!( + "https://oldschool.runescape.wiki/images/{}.png", + item_name_formatted + ); + + let chart_data = generate_chart_data(response.history, time_length, response.item.clone()); + + let chart_url = QuickchartClient::new() + .chart(chart_data) + .version("3".to_string()) + .get_short_url() + .await?; + + print!("Chart URL: {}", chart_url); + + let embed = serenity::CreateEmbed::default() + .title("Grand Exchange Price History") + // .description(format!("**{}**", response.item)) + .description(chart_url.clone()) + .image(chart_url) + .color(0xffd700) + .footer(serenity::CreateEmbedFooter::new( + "Old School RuneScape Grand Exchange", + )) + .thumbnail(image_url); + + ctx.send(poise::CreateReply::default().embed(embed)).await?; + + Ok(()) +} + +pub fn generate_chart_data(history: Vec, time_length: u16, item_name: String) -> String { + let recent_history: Vec<&RSPrice> = history + .iter() + .rev() + .take(time_length as usize) + .collect::>() + .into_iter() + .rev() + .collect(); + + let nice_item_name = item_name.replace('_', " ").replace("-", " "); + + // Extract labels (timestamps) and data (prices) + let labels: Vec = recent_history + .iter() + .map(|price| { + // Format timestamp - handle both string and number formats + match &price.timestamp { + TimeStampValue::String(s) => { + // Try to format ISO 8601 string to a shorter date format + s.split('T').next().unwrap_or(s).to_string() + } + TimeStampValue::Number(n) => { + format!( + "{}", + DateTime::from_timestamp_millis(*n as i64) + .unwrap() + .format("%m-%d") + ) + } + } + }) + .collect(); + + let price_data: Vec = recent_history.iter().map(|price| price.price).collect(); + let volume_data: Vec = recent_history.iter().map(|price| price.volume).collect(); + + let chart_config = json!({ + "type": "line", + "data": { + "labels": labels, + "datasets": [ + { + "label": "Price (gp)", + "data": price_data, + "borderColor": "rgb(255,215,0)", + "backgroundColor": "rgba(255,215,0,0.1)", + "yAxisID": "yLeft" + }, + { + "label": "Volume", + "data": volume_data, + "borderColor": "rgb(75,192,192)", + "backgroundColor": "rgba(75,192,192,0.1)", + "yAxisID": "yRight" + } + ] + }, + "options": { + "responsive": true, + "plugins": { + "title": { + "display": true, + "text": format!("{} Price History", nice_item_name), + }, + "legend": { + "display": true + }, + "tickFormat": { + "notation": "compact", + "maximumFractionDigits": 2 + } + }, + "scales": { + "y": { + "grid": { + "display": true, + "color": "grey", + }, + }, + "x": { + "grid": { + "display": true, + "color": "grey", + }, + }, + "yLeft": { + "type": "linear", + "display": true, + "position": "left", + "stacked": false, + "beginAtZero": false + }, + "yRight": { + "type": "linear", + "display": true, + "position": "right", + "beginAtZero": false, + "grid": { + "drawOnChartArea": false + } + } + } + } + }); + + serde_json::to_string(&chart_config).unwrap_or_else(|_| "{}".to_string()) +} diff --git a/src/commands/shop.rs b/src/commands/shop.rs index 5d88313..632de0f 100644 --- a/src/commands/shop.rs +++ b/src/commands/shop.rs @@ -121,8 +121,8 @@ pub async fn update_shop_count(item_name: String, current_increase: i16, total_i } /// Returns the current amount of an item. -#[poise::command(prefix_command, aliases("count", "getCount"))] -pub async fn item_count( +#[poise::command(prefix_command, aliases("itemCount", "getCount"))] +pub async fn count( ctx: Context<'_>, #[description = "The symbol of the item you want"] symbol: String, ) -> Result<(), Error> { diff --git a/src/main.rs b/src/main.rs index 87aa606..ba4d406 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,32 +1,35 @@ -mod commands; +mod bot_types; mod bot_utils; +mod commands; mod emoji; -mod bot_types; +mod runescape_utils; + // Commands; -use crate::commands::smash::*; +use crate::commands::help::*; use crate::commands::judge::*; -use crate::commands::score::*; use crate::commands::ping::*; -use crate::commands::trade::*; +use crate::commands::runescape::*; +use crate::commands::score::*; use crate::commands::shop::*; -use crate::commands::help::*; +use crate::commands::smash::*; +use crate::commands::trade::*; use crate::bot_types::{Data, Error}; -use std::collections::{HashSet}; -use serenity::http::*; -use serenity::prelude::*; -use serenity::async_trait; -use serenity::model::id::{ChannelId, GuildId, MessageId}; -use serenity::model::channel::{Message, Reaction, ReactionType}; -use serenity::model::gateway::Ready; -use serenity::framework::standard::macros::hook; -use poise::serenity_prelude as serenity_prelude; +use crate::bot_utils::{get_count, is_bot, reset_count}; +use crate::emoji::get_emoji; +use poise::serenity_prelude; use rand::Rng; use serenity::all::Member; +use serenity::async_trait; +use serenity::framework::standard::macros::hook; +use serenity::http::*; use serenity::model::Timestamp; -use crate::bot_utils::{get_count, is_bot, reset_count}; -use crate::emoji::get_emoji; +use serenity::model::channel::{Message, Reaction, ReactionType}; +use serenity::model::gateway::Ready; +use serenity::model::id::{ChannelId, GuildId, MessageId}; +use serenity::prelude::*; +use std::collections::HashSet; struct Handler; @@ -46,41 +49,71 @@ impl EventHandler for Handler { return; } - let mut _rng = rand::rng().random_range(0..200); + let mut _rng = rand::rng().random_range(0..500); let current_number_of_bombs = get_count("mine").await; if _rng <= current_number_of_bombs { let mut member = get_member(_ctx.clone(), msg.clone()).await; let time_out_time = get_time_out_time(); - member.disable_communication_until_datetime(&_ctx.http.clone(), time_out_time).await.unwrap(); + member + .disable_communication_until_datetime(&_ctx.http.clone(), time_out_time) + .await + .unwrap(); reset_count("mine").await; - msg.reply(&_ctx.http, format!("{} You're our lucky loser! See you in 10 minutes. :3", get_emoji("winner"))).await.unwrap(); + msg.reply( + &_ctx.http, + format!( + "{} You're our lucky loser! See you in 10 minutes. :3", + get_emoji("winner") + ), + ) + .await + .unwrap(); } } async fn reaction_add(&self, _ctx: Context, _add_reaction: Reaction) { let reaction = _add_reaction.emoji; - let message= get_message_from_id(_add_reaction.channel_id, _add_reaction.message_id).await.unwrap().author; + let message = get_message_from_id(_add_reaction.channel_id, _add_reaction.message_id) + .await + .unwrap() + .author; let score = get_points_from_emoji(reaction); if _add_reaction.user_id.unwrap().to_string() == message.id.to_string() { + // if (_add_reaction.user_id.unwrap().to_string() == "180083924414758912") { + // let mut memeber = get_member(_ctx.clone(), message.clone()).await; + // } return; } if score == 2 { - bot_utils::plus_two(&_add_reaction.user_id.unwrap().to_string(), &message.id.to_string(), false).await; + bot_utils::plus_two( + &_add_reaction.user_id.unwrap().to_string(), + &message.id.to_string(), + false, + ) + .await; } if score == -2 { - bot_utils::minus_two(&_add_reaction.user_id.unwrap().to_string(), &message.id.to_string(), false).await; + bot_utils::minus_two( + &_add_reaction.user_id.unwrap().to_string(), + &message.id.to_string(), + false, + ) + .await; } bot_utils::score_update(&message.id.to_string(), score).await; - } async fn reaction_remove(&self, _ctx: Context, _removed_reaction: Reaction) { let reaction = _removed_reaction.emoji; - let message= get_message_from_id(_removed_reaction.channel_id, _removed_reaction.message_id).await.unwrap().author; + let message = + get_message_from_id(_removed_reaction.channel_id, _removed_reaction.message_id) + .await + .unwrap() + .author; let score = get_points_from_emoji(reaction); if _removed_reaction.user_id.unwrap().to_string() == message.id.to_string() { @@ -88,23 +121,35 @@ impl EventHandler for Handler { } if score == 2 { - bot_utils::plus_two(&_removed_reaction.user_id.unwrap().to_string(), &message.id.to_string(), true).await; + bot_utils::plus_two( + &_removed_reaction.user_id.unwrap().to_string(), + &message.id.to_string(), + true, + ) + .await; } if score == -2 { - bot_utils::minus_two(&_removed_reaction.user_id.unwrap().to_string(), &message.id.to_string(), true).await; + bot_utils::minus_two( + &_removed_reaction.user_id.unwrap().to_string(), + &message.id.to_string(), + true, + ) + .await; } bot_utils::score_update(&message.id.to_string(), score * -1).await; } async fn ready(&self, _: Context, ready: Ready) { - println!("{} is connected! Environment: {}", ready.user.name, bot_utils::get_env()); + println!( + "{} is connected! Environment: {}", + ready.user.name, + bot_utils::get_env() + ); } - } - /** Returns a time 10 minutes from now. **/ @@ -124,7 +169,7 @@ async fn get_member(_ctx: Context, msg: Message) -> Member { } fn get_points_from_emoji(reaction: ReactionType) -> i16 { - let mut score:i16 = 0; + let mut score: i16 = 0; if reaction == emoji::get_emoji("plus_two") || reaction == emoji::get_emoji("manny") { score = 2; } @@ -152,8 +197,7 @@ async fn main() { | GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::MESSAGE_CONTENT; - let (owners, bot_id) = match http.get_current_application_info().await - { + let (owners, bot_id) = match http.get_current_application_info().await { Ok(info) => { let mut owners = HashSet::new(); if let Some(team) = info.team { @@ -165,20 +209,35 @@ async fn main() { Ok(bot_id) => (owners, bot_id.id), Err(why) => panic!("Could not access the bot id: {:?}", why), } - }, + } Err(why) => panic!("Could not access application info: {:?}", why), }; + // TODO make commands combine vectors from all the command files. let framework = poise::Framework::::builder() .options(poise::FrameworkOptions { - commands: vec![ping(), judge(), score(), top(), leader(), smash(), trade(), wallet(), shop(), item_count(), help()], + commands: vec![ + ping(), + judge(), + score(), + top(), + leader(), + smash(), + trade(), + wallet(), + shop(), + count(), + help(), + grand_exchange(), + grand_exchange_history(), + ], prefix_options: poise::PrefixFrameworkOptions { prefix: Some("!".into()), ..Default::default() }, ..Default::default() }) - .setup(|ctx, _ready, framework|{ + .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data {}) @@ -191,14 +250,13 @@ async fn main() { .event_handler(Handler) .await; client.unwrap().start().await.unwrap(); - - - } -async fn get_message_from_id(channel_id:ChannelId, message_id: MessageId) -> serenity::Result { +async fn get_message_from_id( + channel_id: ChannelId, + message_id: MessageId, +) -> serenity::Result { let token = bot_utils::get_secret(); let http = Http::new(&token); let message = channel_id.message(&http, message_id); return message.await; } - diff --git a/src/runescape_utils/lib.rs b/src/runescape_utils/lib.rs new file mode 100644 index 0000000..c6a9d94 --- /dev/null +++ b/src/runescape_utils/lib.rs @@ -0,0 +1,19 @@ +//! # Rust OSRS Wiki API Wrapper +//! +//! A wrapper for the OSRS Wiki API. +//! +//! I'm only temporaraly moving this here. I want to push a new update. +//! This will all be removed and moved back into the runescape utils crate once i'm done with it. +//! +//! ## Features +//! +//! - Get the price of an item +//! - Get the price history of an item +//! - Get the price of an item in a specific time period +//! - Get the price of an item in a specific time period + +mod rs_client; +pub use rs_client::{ + RSClient, RSItemPrice, RSItemPriceHistory, RSPrice, RSPriceHistoryMapResponse, + RSPriceMapResponse, TimeStampValue, +}; diff --git a/src/runescape_utils/mod.rs b/src/runescape_utils/mod.rs new file mode 100644 index 0000000..19e1292 --- /dev/null +++ b/src/runescape_utils/mod.rs @@ -0,0 +1 @@ +pub mod rs_client; diff --git a/src/runescape_utils/rs_client.rs b/src/runescape_utils/rs_client.rs new file mode 100644 index 0000000..6f83fe8 --- /dev/null +++ b/src/runescape_utils/rs_client.rs @@ -0,0 +1,159 @@ +use reqwest::{Client, Url}; +use serde::{de::Error, Deserialize}; +use std::collections::HashMap; +use thiserror::Error; + +const BASE_URL: &str = "https://api.weirdgloop.org"; +const USER_AGENT: &str = concat!("rust-osrs-wiki-api-wrapper/", env!("CARGO_PKG_VERSION")); + +pub struct RSClient { + client: Client, + base_url: Url, + item_name: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct RSItemPrice { + pub item: String, + pub id: String, + pub timestamp: TimeStampValue, + pub price: u64, + pub volume: u64, +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(untagged)] +pub enum TimeStampValue { + String(String), + Number(u64), +} + +#[derive(Debug, Deserialize, Clone)] +pub struct RSItemPriceHistory { + pub item: String, + pub history: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct RSPrice { + pub id: String, + pub timestamp: TimeStampValue, + pub price: u64, + pub volume: u64, +} +pub type RSPriceMapResponse = HashMap; +pub type RSPriceHistoryMapResponse = HashMap>; + +#[derive(Error, Debug)] +pub enum RSError { + #[error("HTTP error: {0}")] + HttpError(#[from] reqwest::Error), + #[error("Failed to parse JSON response: {0}")] + JsonParseError(#[from] serde_json::Error), + #[error("Failed to parse URL: {0}")] + UrlParseError(#[from] url::ParseError), +} + +impl Default for RSClient { + fn default() -> Self { + Self::new() + } +} + +impl RSClient { + pub fn new() -> Self { + let client = Client::builder() + .user_agent(USER_AGENT) + .build() + .expect("Failed to create HTTP client"); + + RSClient { + client, + base_url: Url::parse(BASE_URL).unwrap(), + item_name: String::new(), + } + } + + pub fn item_name(mut self, item_name: String) -> Self { + self.item_name = item_name; + self + } + + pub async fn get_price(&self) -> Result { + let encoded_name = urlencoding::encode(&self.item_name); + let path = format!( + "/exchange/history/osrs/latest?name={}&lang=en", + encoded_name + ); + + let url = self.base_url.join(&path).map_err(RSError::UrlParseError)?; + let response = self + .client + .get(url) + .send() + .await + .map_err(RSError::HttpError)?; + + let body_text = response.text().await.map_err(RSError::HttpError)?; + + // The API returns a HashMap of item names to price data + let price_map: RSPriceMapResponse = + serde_json::from_str(&body_text).map_err(RSError::JsonParseError)?; + + // Extract the first (and typically only) entry from the map + let (item_name, price_data) = price_map + .into_iter() + .next() + .ok_or_else(|| RSError::JsonParseError(serde_json::Error::custom("empty response")))?; + + // Convert RSPrice to RSItemPrice by adding the item name + let price_response = RSItemPrice { + item: item_name, + id: price_data.id, + timestamp: price_data.timestamp, + price: price_data.price, + volume: price_data.volume, + }; + + Ok(price_response) + } + + pub async fn get_price_history(&self) -> Result { + let encoded_name = urlencoding::encode(&self.item_name); + let path = format!( + "/exchange/history/osrs/last90d?name={}&lang=en", + encoded_name + ); + + let url = self.base_url.join(&path).map_err(RSError::UrlParseError)?; + let response = self + .client + .get(url) + .send() + .await + .map_err(RSError::HttpError)?; + + let body_text = response.text().await.map_err(RSError::HttpError)?; + + // The API returns a HashMap of item names to arrays of price history data + let price_history_map: RSPriceHistoryMapResponse = serde_json::from_str(&body_text) + .map_err(|e| { + eprintln!("JSON parse error at position: {}", e); + eprintln!("Response body: {}", body_text); + RSError::JsonParseError(e) + })?; + + // Extract the first (and typically only) entry from the map + let (item_name, price_history) = price_history_map + .into_iter() + .next() + .ok_or_else(|| RSError::JsonParseError(serde_json::Error::custom("empty response")))?; + + let item_price_history = RSItemPriceHistory { + item: item_name, + history: price_history, + }; + + Ok(item_price_history) + } +} From a0fb8eccd224c9a34866c7c0136f822b65a56efc Mon Sep 17 00:00:00 2001 From: TIm Hillier Date: Sun, 30 Nov 2025 00:07:20 -0800 Subject: [PATCH 3/3] cargo to 4.5 --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index bf11613..a6d125a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "RustBot" -version = "0.4.2" +version = "0.4.5" dependencies = [ "chrono", "poise",