added optimization for leaderboard
This commit is contained in:
343
Cargo.lock
generated
343
Cargo.lock
generated
@@ -28,6 +28,189 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-codec"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-http"
|
||||||
|
version = "3.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
|
||||||
|
dependencies = [
|
||||||
|
"actix-codec",
|
||||||
|
"actix-rt",
|
||||||
|
"actix-service",
|
||||||
|
"actix-utils",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"brotli",
|
||||||
|
"bytes",
|
||||||
|
"bytestring",
|
||||||
|
"derive_more",
|
||||||
|
"encoding_rs",
|
||||||
|
"flate2",
|
||||||
|
"foldhash",
|
||||||
|
"futures-core",
|
||||||
|
"h2 0.3.27",
|
||||||
|
"http 0.2.12",
|
||||||
|
"httparse",
|
||||||
|
"httpdate",
|
||||||
|
"itoa",
|
||||||
|
"language-tags",
|
||||||
|
"local-channel",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"sha1",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-macros"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.111",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-router"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
|
||||||
|
dependencies = [
|
||||||
|
"bytestring",
|
||||||
|
"cfg-if",
|
||||||
|
"http 0.2.12",
|
||||||
|
"regex",
|
||||||
|
"regex-lite",
|
||||||
|
"serde",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-rt"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-server"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
|
||||||
|
dependencies = [
|
||||||
|
"actix-rt",
|
||||||
|
"actix-service",
|
||||||
|
"actix-utils",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"mio",
|
||||||
|
"socket2 0.5.10",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-service"
|
||||||
|
version = "2.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-utils"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
|
||||||
|
dependencies = [
|
||||||
|
"local-waker",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-web"
|
||||||
|
version = "4.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6"
|
||||||
|
dependencies = [
|
||||||
|
"actix-codec",
|
||||||
|
"actix-http",
|
||||||
|
"actix-macros",
|
||||||
|
"actix-router",
|
||||||
|
"actix-rt",
|
||||||
|
"actix-server",
|
||||||
|
"actix-service",
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web-codegen",
|
||||||
|
"bytes",
|
||||||
|
"bytestring",
|
||||||
|
"cfg-if",
|
||||||
|
"cookie",
|
||||||
|
"derive_more",
|
||||||
|
"encoding_rs",
|
||||||
|
"foldhash",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"impl-more",
|
||||||
|
"itoa",
|
||||||
|
"language-tags",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"once_cell",
|
||||||
|
"pin-project-lite",
|
||||||
|
"regex",
|
||||||
|
"regex-lite",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"smallvec",
|
||||||
|
"socket2 0.6.1",
|
||||||
|
"time",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-web-codegen"
|
||||||
|
version = "4.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
|
||||||
|
dependencies = [
|
||||||
|
"actix-router",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.111",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr"
|
name = "addr"
|
||||||
version = "0.15.6"
|
version = "0.15.6"
|
||||||
@@ -94,6 +277,21 @@ dependencies = [
|
|||||||
"equator",
|
"equator",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alloc-no-stdlib"
|
||||||
|
version = "2.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alloc-stdlib"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allocator-api2"
|
name = "allocator-api2"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -604,6 +802,27 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"syn 2.0.111",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brotli"
|
||||||
|
version = "8.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
"alloc-stdlib",
|
||||||
|
"brotli-decompressor",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brotli-decompressor"
|
||||||
|
version = "5.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
|
||||||
|
dependencies = [
|
||||||
|
"alloc-no-stdlib",
|
||||||
|
"alloc-stdlib",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "built"
|
name = "built"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -671,6 +890,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytestring"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "camino"
|
name = "camino"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -875,6 +1103,26 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -1108,6 +1356,29 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||||
|
dependencies = [
|
||||||
|
"derive_more-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more-impl"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version",
|
||||||
|
"syn 2.0.111",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deunicode"
|
name = "deunicode"
|
||||||
version = "1.6.2"
|
version = "1.6.2"
|
||||||
@@ -2239,6 +2510,12 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
|
checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "impl-more"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@@ -2406,6 +2683,12 @@ dependencies = [
|
|||||||
"regex-automata",
|
"regex-automata",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "language-tags"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -2489,6 +2772,23 @@ version = "0.8.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "local-channel"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"local-waker",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "local-waker"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -2669,6 +2969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
@@ -3713,6 +4014,12 @@ dependencies = [
|
|||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-lite"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
@@ -5535,6 +5842,12 @@ dependencies = [
|
|||||||
"unicode-script",
|
"unicode-script",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
@@ -5641,10 +5954,12 @@ name = "void-sentinel"
|
|||||||
version = "0.1.0-alpha"
|
version = "0.1.0-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"futures",
|
||||||
"image",
|
"image",
|
||||||
"imageproc",
|
"imageproc",
|
||||||
"poise",
|
"poise",
|
||||||
@@ -6403,6 +6718,34 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"syn 2.0.111",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-safe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-safe"
|
||||||
|
version = "7.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||||
|
dependencies = [
|
||||||
|
"zstd-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zstd-sys"
|
||||||
|
version = "2.0.16+zstd.1.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ab_glyph = "0.2.32"
|
ab_glyph = "0.2.32"
|
||||||
|
actix-web = "4.9"
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
image = { version = "0.25.9", features = ["webp"] }
|
image = { version = "0.25.9", features = ["webp", "jpeg"] }
|
||||||
imageproc = "0.25.0"
|
imageproc = "0.25.0"
|
||||||
poise = "0.6.1"
|
poise = "0.6.1"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
|
|||||||
@@ -6,3 +6,8 @@ SURREAL_PASS=""
|
|||||||
SURREAL_NS=""
|
SURREAL_NS=""
|
||||||
SURREAL_DB=""
|
SURREAL_DB=""
|
||||||
NVIDIA_API=""
|
NVIDIA_API=""
|
||||||
|
OLLAMA_SERVER_URL=""
|
||||||
|
OLLAMA_MODEL=""
|
||||||
|
AI_CHAT_COOLDOWN_MS=""
|
||||||
|
BOT_OWNER_ID=""
|
||||||
|
IGNORE_RUDE=""
|
||||||
355
src/api.rs
Normal file
355
src/api.rs
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
use actix_web::{web, App, HttpServer, HttpResponse, post, get, error, body::EitherBody, dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}};
|
||||||
|
use futures::future::LocalBoxFuture;
|
||||||
|
use futures::FutureExt;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serenity::cache::Cache;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use surrealdb::Surreal;
|
||||||
|
use surrealdb::engine::remote::ws::Client;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct IsBotThereRequest {
|
||||||
|
pub guild_ids: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct IsBotThereResponse {
|
||||||
|
pub results: Vec<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApiState {
|
||||||
|
pub cache: Arc<Cache>,
|
||||||
|
pub db: Surreal<Client>,
|
||||||
|
pub api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct LeaderboardMember {
|
||||||
|
pub user_id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub avatar: String,
|
||||||
|
pub level: u64,
|
||||||
|
pub xp: u64,
|
||||||
|
pub rank: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Role {
|
||||||
|
pub role_id: String,
|
||||||
|
pub role_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct LevelRole {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct GuildRecord {
|
||||||
|
level_role_stack: Option<std::collections::HashMap<String, Vec<u64>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct LeaderboardEntry {
|
||||||
|
id: surrealdb::sql::Thing,
|
||||||
|
xp: u64,
|
||||||
|
level: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// API Key Authentication Middleware
|
||||||
|
pub struct ApiKeyMiddleware {
|
||||||
|
api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiKeyMiddleware {
|
||||||
|
pub fn new(api_key: String) -> Self {
|
||||||
|
ApiKeyMiddleware { api_key }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B> Transform<S, ServiceRequest> for ApiKeyMiddleware
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = error::Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<EitherBody<B>>;
|
||||||
|
type Error = error::Error;
|
||||||
|
type InitError = ();
|
||||||
|
type Transform = ApiKeyMiddlewareService<S>;
|
||||||
|
type Future = futures::future::Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
futures::future::ok(ApiKeyMiddlewareService {
|
||||||
|
service,
|
||||||
|
api_key: self.api_key.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApiKeyMiddlewareService<S> {
|
||||||
|
service: S,
|
||||||
|
api_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B> Service<ServiceRequest> for ApiKeyMiddlewareService<S>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = error::Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<EitherBody<B>>;
|
||||||
|
type Error = error::Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
let api_key = self.api_key.clone();
|
||||||
|
let path = req.path().to_string();
|
||||||
|
|
||||||
|
// Check API key from header
|
||||||
|
let header_key = req
|
||||||
|
.headers()
|
||||||
|
.get("X-API-Key")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if header_key.is_empty() || header_key != api_key {
|
||||||
|
warn!("Unauthorized API request to {} - missing or invalid API key", path);
|
||||||
|
return Box::pin(async move {
|
||||||
|
Err(error::ErrorUnauthorized("Missing or invalid API key"))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Authorized API request to {}", path);
|
||||||
|
|
||||||
|
Box::pin(
|
||||||
|
self.service
|
||||||
|
.call(req)
|
||||||
|
.then(|res: Result<ServiceResponse<B>, error::Error>| async move {
|
||||||
|
Ok(res?.map_into_left_body())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/api/is_bot_there")]
|
||||||
|
async fn is_bot_there(
|
||||||
|
request_body: web::Json<IsBotThereRequest>,
|
||||||
|
data: web::Data<ApiState>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
info!(
|
||||||
|
"Processing /api/is_bot_there request with {} guild IDs",
|
||||||
|
request_body.guild_ids.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let results: Vec<bool> = request_body
|
||||||
|
.guild_ids
|
||||||
|
.iter()
|
||||||
|
.map(|guild_id| {
|
||||||
|
let guild_exists = data.cache.guild(*guild_id).is_some();
|
||||||
|
guild_exists
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let found_count = results.iter().filter(|&&b| b).count();
|
||||||
|
info!(
|
||||||
|
"Bot found in {}/{} requested guilds",
|
||||||
|
found_count,
|
||||||
|
results.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(IsBotThereResponse { results }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/{guild_id}/leaderboard")]
|
||||||
|
async fn get_leaderboard(
|
||||||
|
guild_id: web::Path<u64>,
|
||||||
|
data: web::Data<ApiState>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let guild_id_value = guild_id.into_inner();
|
||||||
|
info!("Processing /api/{}/leaderboard request", guild_id_value);
|
||||||
|
|
||||||
|
// Check if bot is in the guild
|
||||||
|
if data.cache.guild(guild_id_value).is_none() {
|
||||||
|
warn!("Bot is not in guild {}", guild_id_value);
|
||||||
|
return Err(error::ErrorNotFound("Bot is not in this guild"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query all users for this guild, ordered by level and xp
|
||||||
|
let sql = "SELECT * FROM levels WHERE string::starts_with(record::id(id), $prefix) ORDER BY level DESC, xp DESC";
|
||||||
|
let prefix = format!("{}:", guild_id_value);
|
||||||
|
|
||||||
|
let mut response = data.db.query(sql).bind(("prefix", prefix)).await
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("Database query error: {}", e);
|
||||||
|
error::ErrorInternalServerError("Database query failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let entries: Vec<LeaderboardEntry> = response.take(0)
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("Failed to parse database response: {}", e);
|
||||||
|
error::ErrorInternalServerError("Failed to parse database response")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if entries.is_empty() {
|
||||||
|
info!("No leaderboard data found for guild {}", guild_id_value);
|
||||||
|
return Ok(HttpResponse::Ok().json(Vec::<LeaderboardMember>::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Found {} members in leaderboard for guild {}", entries.len(), guild_id_value);
|
||||||
|
|
||||||
|
// Fetch user data for all entries
|
||||||
|
let mut leaderboard: Vec<LeaderboardMember> = Vec::new();
|
||||||
|
|
||||||
|
for (i, entry) in entries.iter().enumerate() {
|
||||||
|
// Extract user id from Surreal Thing
|
||||||
|
let id_value = &entry.id.id;
|
||||||
|
let clean_id_str = match id_value {
|
||||||
|
surrealdb::sql::Id::String(s) => s.as_str().to_string(),
|
||||||
|
_ => entry.id.id.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let parts: Vec<&str> = clean_id_str.split(':').collect();
|
||||||
|
let user_id_str = parts.last().unwrap_or(&"0");
|
||||||
|
let user_id_u64 = user_id_str.parse::<u64>().unwrap_or(0);
|
||||||
|
|
||||||
|
if user_id_u64 == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut username = String::from("Unknown User");
|
||||||
|
let mut avatar_url = String::new();
|
||||||
|
|
||||||
|
let user_id = serenity::all::UserId::new(user_id_u64);
|
||||||
|
|
||||||
|
// Try to get member info from cache first
|
||||||
|
if let Some(guild) = data.cache.guild(guild_id_value) {
|
||||||
|
if let Some(member) = guild.members.get(&user_id) {
|
||||||
|
username = member.display_name().to_string();
|
||||||
|
avatar_url = member.user.face();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in cache, we'll just use Unknown User
|
||||||
|
if avatar_url.is_empty() {
|
||||||
|
info!("User {} not in cache for guild {}", user_id_u64, guild_id_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderboard.push(LeaderboardMember {
|
||||||
|
user_id: user_id_u64.to_string(),
|
||||||
|
username,
|
||||||
|
avatar: avatar_url,
|
||||||
|
level: entry.level,
|
||||||
|
xp: entry.xp,
|
||||||
|
rank: i + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Returning {} members in leaderboard", leaderboard.len());
|
||||||
|
Ok(HttpResponse::Ok().json(leaderboard))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/{guild_id}/roles")]
|
||||||
|
async fn get_roles(
|
||||||
|
guild_id: web::Path<u64>,
|
||||||
|
data: web::Data<ApiState>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let guild_id_value = guild_id.into_inner();
|
||||||
|
info!("Processing /api/{}/roles request", guild_id_value);
|
||||||
|
|
||||||
|
// Check if bot is in the guild
|
||||||
|
let guild = data.cache.guild(guild_id_value)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
warn!("Bot is not in guild {}", guild_id_value);
|
||||||
|
error::ErrorNotFound("Bot is not in this guild")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Get all roles from the guild
|
||||||
|
let roles: Vec<Role> = guild.roles
|
||||||
|
.iter()
|
||||||
|
.map(|(role_id, role)| Role {
|
||||||
|
role_id: role_id.to_string(),
|
||||||
|
role_name: role.name.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
info!("Returning {} roles for guild {}", roles.len(), guild_id_value);
|
||||||
|
Ok(HttpResponse::Ok().json(roles))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/{guild_id}/level/track")]
|
||||||
|
async fn get_level_tracks(
|
||||||
|
guild_id: web::Path<u64>,
|
||||||
|
data: web::Data<ApiState>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let guild_id_value = guild_id.into_inner();
|
||||||
|
info!("Processing /api/{}/level/track request", guild_id_value);
|
||||||
|
|
||||||
|
// Check if bot is in the guild
|
||||||
|
let guild = data.cache.guild(guild_id_value)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
warn!("Bot is not in guild {}", guild_id_value);
|
||||||
|
error::ErrorNotFound("Bot is not in this guild")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Query the guild record from database
|
||||||
|
let record: Option<GuildRecord> = data.db.select(("guilds", guild_id_value.to_string())).await
|
||||||
|
.map_err(|e| {
|
||||||
|
warn!("Database query error: {}", e);
|
||||||
|
error::ErrorInternalServerError("Database query failed")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut result: std::collections::HashMap<String, Vec<LevelRole>> = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
if let Some(record) = record {
|
||||||
|
if let Some(level_role_stack) = record.level_role_stack {
|
||||||
|
for (track_name, role_ids) in level_role_stack {
|
||||||
|
let mut roles: Vec<LevelRole> = Vec::new();
|
||||||
|
for role_id in role_ids {
|
||||||
|
let role_id_obj = serenity::all::RoleId::new(role_id);
|
||||||
|
let role_name = guild.roles.get(&role_id_obj)
|
||||||
|
.map(|r| r.name.clone())
|
||||||
|
.unwrap_or_else(|| "Unknown Role".to_string());
|
||||||
|
|
||||||
|
roles.push(LevelRole {
|
||||||
|
id: role_id.to_string(),
|
||||||
|
name: role_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result.insert(track_name, roles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Returning {} level tracks for guild {}", result.len(), guild_id_value);
|
||||||
|
Ok(HttpResponse::Ok().json(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_api_server(
|
||||||
|
cache: Arc<Cache>,
|
||||||
|
db: Surreal<Client>,
|
||||||
|
api_key: String,
|
||||||
|
port: u16,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
info!("Starting API server on port {}", port);
|
||||||
|
|
||||||
|
let state = web::Data::new(ApiState { cache, db, api_key: api_key.clone() });
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.app_data(state.clone())
|
||||||
|
.wrap(ApiKeyMiddleware::new(api_key.clone()))
|
||||||
|
.service(is_bot_there)
|
||||||
|
.service(get_leaderboard)
|
||||||
|
.service(get_roles)
|
||||||
|
.service(get_level_tracks)
|
||||||
|
})
|
||||||
|
.bind(("0.0.0.0", port))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
@@ -1,18 +1,42 @@
|
|||||||
use crate::{Context, Error};
|
use crate::{Context, Error};
|
||||||
use ab_glyph::{FontRef, PxScale};
|
use ab_glyph::{FontRef, PxScale};
|
||||||
use image::{ImageBuffer, Rgba};
|
use image::{ImageBuffer, Rgba, DynamicImage};
|
||||||
use imageproc::drawing::{draw_filled_rect_mut, draw_text_mut};
|
use imageproc::drawing::{draw_filled_rect_mut, draw_filled_circle_mut, draw_text_mut};
|
||||||
use imageproc::rect::Rect;
|
use imageproc::rect::Rect;
|
||||||
use poise::serenity_prelude as serenity;
|
use poise::serenity_prelude as serenity;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serenity::Mentionable;
|
use serenity::Mentionable;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::sync::{Arc, OnceLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use serenity::prelude::TypeMapKey;
|
use serenity::prelude::TypeMapKey;
|
||||||
use surrealdb::Surreal;
|
use surrealdb::Surreal;
|
||||||
use surrealdb::engine::remote::ws::Client;
|
use surrealdb::engine::remote::ws::Client;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
/// Global avatar cache - stores decoded images by URL
|
||||||
|
/// TTL is handled by periodic cleanup or could use moka crate
|
||||||
|
static AVATAR_CACHE: OnceLock<Arc<RwLock<HashMap<String, Arc<DynamicImage>>>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn get_avatar_cache() -> &'static Arc<RwLock<HashMap<String, Arc<DynamicImage>>>> {
|
||||||
|
AVATAR_CACHE.get_or_init(|| Arc::new(RwLock::new(HashMap::new())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global HTTP client for avatar fetches - connection pooling
|
||||||
|
static AVATAR_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||||||
|
|
||||||
|
fn get_avatar_client() -> &'static reqwest::Client {
|
||||||
|
AVATAR_CLIENT.get_or_init(|| {
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_millis(800))
|
||||||
|
.pool_max_idle_per_host(10)
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DbKey;
|
pub struct DbKey;
|
||||||
|
|
||||||
@@ -516,7 +540,9 @@ struct LeaderboardRenderEntry {
|
|||||||
level: u64,
|
level: u64,
|
||||||
xp: u64,
|
xp: u64,
|
||||||
next_level_xp: u64,
|
next_level_xp: u64,
|
||||||
avatar: Option<image::DynamicImage>,
|
avatar: Option<Arc<DynamicImage>>,
|
||||||
|
/// Used to generate a colored placeholder if avatar is missing
|
||||||
|
user_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command, prefix_command, guild_only)]
|
#[poise::command(slash_command, prefix_command, guild_only)]
|
||||||
@@ -542,13 +568,22 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Fetch all user data and avatars in parallel
|
// 1. Fetch all user data and avatars in parallel
|
||||||
|
let avatar_client = get_avatar_client();
|
||||||
|
let avatar_cache = get_avatar_cache();
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
// Try to get guild from cache first (much faster than API calls)
|
||||||
|
let cached_guild = guild_id.to_guild_cached(&ctx.serenity_context().cache).map(|g| g.clone());
|
||||||
|
|
||||||
for (i, entry) in entries.iter().enumerate() {
|
for (i, entry) in entries.iter().enumerate() {
|
||||||
let ctx = ctx.clone(); // Clone for async
|
let http = ctx.serenity_context().http.clone();
|
||||||
let entry_level = entry.level;
|
let entry_level = entry.level;
|
||||||
let entry_xp = entry.xp;
|
let entry_xp = entry.xp;
|
||||||
let guild_id = guild_id; // capture
|
let guild_id = guild_id;
|
||||||
|
let avatar_client = avatar_client.clone();
|
||||||
|
let avatar_cache = avatar_cache.clone();
|
||||||
|
let cached_guild = cached_guild.clone();
|
||||||
|
|
||||||
// Extract user id robustly from Surreal `Thing`
|
// Extract user id robustly from Surreal `Thing`
|
||||||
let id_value = &entry.id.id;
|
let id_value = &entry.id.id;
|
||||||
@@ -567,38 +602,78 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
|
|
||||||
if user_id_u64 != 0 {
|
if user_id_u64 != 0 {
|
||||||
let user_id = serenity::UserId::new(user_id_u64);
|
let user_id = serenity::UserId::new(user_id_u64);
|
||||||
// Prefer guild display name (nickname) when available
|
|
||||||
if let Ok(member) = ctx.http().get_member(guild_id, user_id).await {
|
// Try cache first (instant), then fall back to API
|
||||||
|
let member_from_cache = cached_guild.as_ref()
|
||||||
|
.and_then(|g| g.members.get(&user_id).cloned());
|
||||||
|
|
||||||
|
if let Some(member) = member_from_cache {
|
||||||
user_name = member.display_name().to_string();
|
user_name = member.display_name().to_string();
|
||||||
let face = member.user.face();
|
avatar_url = get_small_avatar_url(&member.user);
|
||||||
avatar_url = normalize_avatar_url(&face);
|
} else if let Ok(member) = http.get_member(guild_id, user_id).await {
|
||||||
} else {
|
user_name = member.display_name().to_string();
|
||||||
// Fallback to user object
|
avatar_url = get_small_avatar_url(&member.user);
|
||||||
if let Ok(user) = ctx.http().get_user(user_id).await {
|
} else if let Ok(user) = http.get_user(user_id).await {
|
||||||
user_name = user
|
user_name = user.global_name.as_ref().unwrap_or(&user.name).to_string();
|
||||||
.global_name
|
avatar_url = get_small_avatar_url(&user);
|
||||||
.as_ref()
|
|
||||||
.unwrap_or(&user.name)
|
|
||||||
.to_string();
|
|
||||||
let face = user.face();
|
|
||||||
avatar_url = normalize_avatar_url(&face);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// keep defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch avatar image if we have a URL
|
// Check avatar cache first
|
||||||
let mut avatar_img = None;
|
let avatar_img: Option<Arc<DynamicImage>> = if !avatar_url.is_empty() {
|
||||||
if !avatar_url.is_empty() {
|
// Fast path: check cache
|
||||||
if let Ok(response) = reqwest::get(&avatar_url).await {
|
{
|
||||||
if let Ok(bytes) = response.bytes().await {
|
let cache = avatar_cache.read().await;
|
||||||
|
if let Some(cached) = cache.get(&avatar_url) {
|
||||||
|
Some(cached.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}.or_else(|| None).map(Some).unwrap_or_else(|| {
|
||||||
|
// Cache miss - need to fetch (will be done below)
|
||||||
|
None
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// If not in cache, fetch with short timeout
|
||||||
|
let avatar_img = if avatar_img.is_some() {
|
||||||
|
avatar_img
|
||||||
|
} else if !avatar_url.is_empty() {
|
||||||
|
// Use tokio timeout for precise control
|
||||||
|
let fetch_result = tokio::time::timeout(
|
||||||
|
Duration::from_millis(500),
|
||||||
|
async {
|
||||||
|
match avatar_client.get(&avatar_url).send().await {
|
||||||
|
Ok(response) => response.bytes().await.ok(),
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).await;
|
||||||
|
|
||||||
|
match fetch_result {
|
||||||
|
Ok(Some(bytes)) => {
|
||||||
if let Ok(img) = image::load_from_memory(&bytes) {
|
if let Ok(img) = image::load_from_memory(&bytes) {
|
||||||
avatar_img = Some(img);
|
let arc_img = Arc::new(img);
|
||||||
|
// Store in cache for next time
|
||||||
|
{
|
||||||
|
let mut cache = avatar_cache.write().await;
|
||||||
|
// Limit cache size to prevent memory issues
|
||||||
|
if cache.len() < 500 {
|
||||||
|
cache.insert(avatar_url, arc_img.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(arc_img)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
LeaderboardRenderEntry {
|
LeaderboardRenderEntry {
|
||||||
username: user_name,
|
username: user_name,
|
||||||
@@ -607,6 +682,7 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
xp: entry_xp,
|
xp: entry_xp,
|
||||||
next_level_xp: (entry_level + 1) * 100,
|
next_level_xp: (entry_level + 1) * 100,
|
||||||
avatar: avatar_img,
|
avatar: avatar_img,
|
||||||
|
user_id: user_id_u64,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -622,7 +698,7 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
ctx.send(
|
ctx.send(
|
||||||
poise::CreateReply::default().attachment(serenity::CreateAttachment::bytes(
|
poise::CreateReply::default().attachment(serenity::CreateAttachment::bytes(
|
||||||
image_data,
|
image_data,
|
||||||
"leaderboard.png",
|
"leaderboard.jpg",
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -630,30 +706,54 @@ pub async fn leaderboard(ctx: Context<'_>) -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_avatar_url(url: &str) -> String {
|
/// Get a small avatar URL (48px) - webp is faster to download & decode
|
||||||
if url.is_empty() { return String::new(); }
|
fn get_small_avatar_url(user: &serenity::User) -> String {
|
||||||
// Prefer PNG to ensure decoder compatibility; preserve size if present
|
// Use webp format (smaller file size, faster download) and small size
|
||||||
// Replace extension .webp -> .png, and enforce format=png when query exists
|
// The image crate has webp support enabled
|
||||||
// Simple approach: if it contains ".webp", swap to ".png"; also add "?size=128" if none
|
match &user.avatar {
|
||||||
let mut out = url.replace(".webp", ".png");
|
Some(hash) => {
|
||||||
if !out.contains("format=") && out.contains("cdn.discordapp.com") {
|
let ext = if hash.is_animated() { "gif" } else { "webp" };
|
||||||
if out.contains('?') { out.push_str("&format=png"); } else { out.push_str("?format=png"); }
|
format!(
|
||||||
|
"https://cdn.discordapp.com/avatars/{}/{}.{}?size=48",
|
||||||
|
user.id, hash, ext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Default avatar - use small size
|
||||||
|
let index = if let Some(discrim) = user.discriminator {
|
||||||
|
discrim.get() % 5
|
||||||
|
} else {
|
||||||
|
((user.id.get() >> 22) % 6) as u16
|
||||||
|
};
|
||||||
|
format!(
|
||||||
|
"https://cdn.discordapp.com/embed/avatars/{}.png?size=48",
|
||||||
|
index
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if !out.contains("size=") {
|
|
||||||
if out.contains('?') { out.push_str("&size=128"); } else { out.push_str("?size=128"); }
|
|
||||||
}
|
}
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pre-defined colors for avatar placeholders based on user ID
|
||||||
|
const PLACEHOLDER_COLORS: [Rgba<u8>; 8] = [
|
||||||
|
Rgba([114, 137, 218, 255]), // Blurple
|
||||||
|
Rgba([67, 181, 129, 255]), // Green
|
||||||
|
Rgba([250, 166, 26, 255]), // Yellow
|
||||||
|
Rgba([240, 71, 71, 255]), // Red
|
||||||
|
Rgba([255, 115, 250, 255]), // Pink
|
||||||
|
Rgba([26, 188, 156, 255]), // Teal
|
||||||
|
Rgba([230, 126, 34, 255]), // Orange
|
||||||
|
Rgba([155, 89, 182, 255]), // Purple
|
||||||
|
];
|
||||||
|
|
||||||
fn generate_leaderboard_image(
|
fn generate_leaderboard_image(
|
||||||
entries: Vec<LeaderboardRenderEntry>,
|
entries: Vec<LeaderboardRenderEntry>,
|
||||||
) -> Result<Vec<u8>, Error> {
|
) -> Result<Vec<u8>, Error> {
|
||||||
// Image dimensions
|
// Image dimensions
|
||||||
let width = 800;
|
let width = 800u32;
|
||||||
let height = 100 + (entries.len() as u32 * 80); // Header + rows
|
let height = 100 + (entries.len() as u32 * 80); // Header + rows
|
||||||
let mut image = ImageBuffer::from_pixel(width, height, Rgba([40, 44, 52, 255])); // Dark background
|
let mut image = ImageBuffer::from_pixel(width, height, Rgba([40, 44, 52, 255])); // Dark background
|
||||||
|
|
||||||
// Load font
|
// Load font once (compiled into binary)
|
||||||
let font_bytes = include_bytes!("../assets/Roboto-Regular.ttf");
|
let font_bytes = include_bytes!("../assets/Roboto-Regular.ttf");
|
||||||
let font = FontRef::try_from_slice(font_bytes)?;
|
let font = FontRef::try_from_slice(font_bytes)?;
|
||||||
|
|
||||||
@@ -683,11 +783,33 @@ fn generate_leaderboard_image(
|
|||||||
&format!("#{}", entry.rank),
|
&format!("#{}", entry.rank),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw Avatar
|
// Draw Avatar or colored circle placeholder
|
||||||
|
let avatar_x = 80i64;
|
||||||
|
let avatar_y = y_offset as i64 + 16;
|
||||||
|
|
||||||
if let Some(avatar_img) = &entry.avatar {
|
if let Some(avatar_img) = &entry.avatar {
|
||||||
let avatar_resized =
|
// Use Nearest filter - fastest possible, good enough for small avatars
|
||||||
avatar_img.resize(60, 60, image::imageops::FilterType::Lanczos3);
|
let avatar_resized = avatar_img.resize_exact(48, 48, image::imageops::FilterType::Nearest);
|
||||||
image::imageops::overlay(&mut image, &avatar_resized, 80, y_offset as i64 + 10);
|
image::imageops::overlay(&mut image, &avatar_resized, avatar_x, avatar_y);
|
||||||
|
} else {
|
||||||
|
// Draw colored circle placeholder based on user ID
|
||||||
|
let color_idx = (entry.user_id % 8) as usize;
|
||||||
|
let color = PLACEHOLDER_COLORS[color_idx];
|
||||||
|
let center_x = (avatar_x + 24) as i32;
|
||||||
|
let center_y = (avatar_y + 24) as i32;
|
||||||
|
draw_filled_circle_mut(&mut image, (center_x, center_y), 24, color);
|
||||||
|
|
||||||
|
// Draw first letter of username
|
||||||
|
let first_char = entry.username.chars().next().unwrap_or('?').to_uppercase().to_string();
|
||||||
|
draw_text_mut(
|
||||||
|
&mut image,
|
||||||
|
white,
|
||||||
|
center_x - 8,
|
||||||
|
center_y - 12,
|
||||||
|
scale_text,
|
||||||
|
&font,
|
||||||
|
&first_char,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw Username
|
// Draw Username
|
||||||
@@ -713,14 +835,14 @@ fn generate_leaderboard_image(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Draw XP Bar
|
// Draw XP Bar
|
||||||
let bar_width = 300;
|
let bar_width = 300u32;
|
||||||
let bar_height = 20;
|
let bar_height = 20u32;
|
||||||
let bar_x = 450;
|
let bar_x = 450i32;
|
||||||
let bar_y = y_offset + 30;
|
let bar_y = y_offset as i32 + 30;
|
||||||
|
|
||||||
draw_filled_rect_mut(
|
draw_filled_rect_mut(
|
||||||
&mut image,
|
&mut image,
|
||||||
Rect::at(bar_x as i32, bar_y as i32).of_size(bar_width, bar_height),
|
Rect::at(bar_x, bar_y).of_size(bar_width, bar_height),
|
||||||
bar_bg,
|
bar_bg,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -734,7 +856,7 @@ fn generate_leaderboard_image(
|
|||||||
if fill_width > 0 {
|
if fill_width > 0 {
|
||||||
draw_filled_rect_mut(
|
draw_filled_rect_mut(
|
||||||
&mut image,
|
&mut image,
|
||||||
Rect::at(bar_x as i32, bar_y as i32).of_size(fill_width, bar_height),
|
Rect::at(bar_x, bar_y).of_size(fill_width, bar_height),
|
||||||
bar_fill,
|
bar_fill,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -744,16 +866,20 @@ fn generate_leaderboard_image(
|
|||||||
draw_text_mut(
|
draw_text_mut(
|
||||||
&mut image,
|
&mut image,
|
||||||
white,
|
white,
|
||||||
bar_x as i32 + 5,
|
bar_x + 5,
|
||||||
bar_y as i32 + 2, // Centering vertically roughly
|
bar_y + 2,
|
||||||
PxScale::from(14.0),
|
PxScale::from(14.0),
|
||||||
&font,
|
&font,
|
||||||
&xp_text,
|
&xp_text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut bytes: Vec<u8> = Vec::new();
|
// Use JPEG for much faster encoding (PNG is slow)
|
||||||
image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
|
// Quality 85 is a good balance of size vs quality
|
||||||
|
let mut bytes: Vec<u8> = Vec::with_capacity(width as usize * height as usize);
|
||||||
|
let rgb_image = DynamicImage::ImageRgba8(image).into_rgb8();
|
||||||
|
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut bytes, 85);
|
||||||
|
encoder.encode_image(&rgb_image)?;
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ impl EventHandler for Handler {
|
|||||||
if let Err(e) = crate::commands::utility::process_auto_response(&ctx, &msg).await {
|
if let Err(e) = crate::commands::utility::process_auto_response(&ctx, &msg).await {
|
||||||
tracing::error!("Error processing message for auto-response: {}", e);
|
tracing::error!("Error processing message for auto-response: {}", e);
|
||||||
}
|
}
|
||||||
|
if let Err(e) = crate::commands::fun::handle_ai_chat(&ctx, &msg).await {
|
||||||
|
tracing::error!("Error processing message for AI chat: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn guild_member_update(
|
async fn guild_member_update(
|
||||||
|
|||||||
73
src/main.rs
73
src/main.rs
@@ -1,10 +1,11 @@
|
|||||||
|
mod api;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod listener;
|
mod listener;
|
||||||
|
|
||||||
use ::serenity::all::GatewayIntents;
|
use ::serenity::all::{GatewayIntents, UserId};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use poise::{Framework, FrameworkOptions, serenity_prelude as serenity};
|
use poise::{Framework, FrameworkOptions, serenity_prelude as serenity};
|
||||||
use std::{env, sync::Arc};
|
use std::{collections::HashSet, env, sync::Arc};
|
||||||
use surrealdb::Surreal;
|
use surrealdb::Surreal;
|
||||||
use surrealdb::engine::remote::ws::{Client, Wss};
|
use surrealdb::engine::remote::ws::{Client, Wss};
|
||||||
use surrealdb::opt::auth::Root;
|
use surrealdb::opt::auth::Root;
|
||||||
@@ -16,6 +17,7 @@ type Context<'a> = poise::Context<'a, Data, Error>;
|
|||||||
|
|
||||||
struct Data {
|
struct Data {
|
||||||
db: Surreal<Client>,
|
db: Surreal<Client>,
|
||||||
|
ai_chat: Arc<commands::fun::AiChatManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main()]
|
#[tokio::main()]
|
||||||
@@ -32,6 +34,8 @@ async fn main() -> Result<(), Error> {
|
|||||||
|
|
||||||
let token = env::var("DISCORD_TOKEN")?;
|
let token = env::var("DISCORD_TOKEN")?;
|
||||||
|
|
||||||
|
let api_key = env::var("API_KEY").expect("Expected API_KEY in environment");
|
||||||
|
|
||||||
let surreal_address =
|
let surreal_address =
|
||||||
env::var("SURREAL_ADDRESS").expect("Expected SURREAL_ADDRESS in environment");
|
env::var("SURREAL_ADDRESS").expect("Expected SURREAL_ADDRESS in environment");
|
||||||
let surreal_user = env::var("SURREAL_USER").expect("Expected SURREAL_USER in environment");
|
let surreal_user = env::var("SURREAL_USER").expect("Expected SURREAL_USER in environment");
|
||||||
@@ -39,6 +43,28 @@ async fn main() -> Result<(), Error> {
|
|||||||
let surreal_ns = env::var("SURREAL_NS").expect("Expected SURREAL_NS in environment");
|
let surreal_ns = env::var("SURREAL_NS").expect("Expected SURREAL_NS in environment");
|
||||||
let surreal_db = env::var("SURREAL_DB").expect("Expected SURREAL_DB in environment");
|
let surreal_db = env::var("SURREAL_DB").expect("Expected SURREAL_DB in environment");
|
||||||
|
|
||||||
|
let ollama_url = env::var("OLLAMA_SERVER_URL").expect("Expected OLLAMA_SERVER_URL in environment");
|
||||||
|
let ollama_model = env::var("OLLAMA_MODEL").unwrap_or_else(|_| "llama3".to_string());
|
||||||
|
let ai_chat_cooldown_ms = env::var("AI_CHAT_COOLDOWN_MS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse::<u64>().ok())
|
||||||
|
.unwrap_or(1500);
|
||||||
|
|
||||||
|
let ignore_rude: Vec<String> = env::var("IGNORE_RUDE")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let ai_chat_manager = Arc::new(commands::fun::AiChatManager::new(
|
||||||
|
ollama_url,
|
||||||
|
ollama_model,
|
||||||
|
std::time::Duration::from_millis(ai_chat_cooldown_ms),
|
||||||
|
ignore_rude,
|
||||||
|
));
|
||||||
|
|
||||||
let db = Surreal::new::<Wss>(&surreal_address).await?;
|
let db = Surreal::new::<Wss>(&surreal_address).await?;
|
||||||
|
|
||||||
db.signin(Root {
|
db.signin(Root {
|
||||||
@@ -51,8 +77,17 @@ async fn main() -> Result<(), Error> {
|
|||||||
db.use_ns(&surreal_ns).use_db(&surreal_db).await?;
|
db.use_ns(&surreal_ns).use_db(&surreal_db).await?;
|
||||||
|
|
||||||
let db_clone = db.clone();
|
let db_clone = db.clone();
|
||||||
|
let ai_chat_clone = ai_chat_manager.clone();
|
||||||
|
|
||||||
|
let owner_id = env::var("BOT_OWNER_ID")
|
||||||
|
.expect("Expected BOT_OWNER_ID in environment")
|
||||||
|
.parse::<u64>()?;
|
||||||
|
let mut owners = HashSet::new();
|
||||||
|
owners.insert(UserId::new(owner_id));
|
||||||
|
|
||||||
let framework = Framework::builder()
|
let framework = Framework::builder()
|
||||||
.options(FrameworkOptions::<Data, Error> {
|
.options(FrameworkOptions::<Data, Error> {
|
||||||
|
owners,
|
||||||
commands: vec![
|
commands: vec![
|
||||||
commands::level::set_level_roles(),
|
commands::level::set_level_roles(),
|
||||||
commands::level::get_level_roles(),
|
commands::level::get_level_roles(),
|
||||||
@@ -62,10 +97,12 @@ async fn main() -> Result<(), Error> {
|
|||||||
commands::level::levelup_role_bridger(),
|
commands::level::levelup_role_bridger(),
|
||||||
commands::fun::say(),
|
commands::fun::say(),
|
||||||
commands::fun::urban(),
|
commands::fun::urban(),
|
||||||
|
commands::fun::ai_chat(),
|
||||||
commands::utility::auto_response(),
|
commands::utility::auto_response(),
|
||||||
commands::utility::view_auto_responses(),
|
commands::utility::view_auto_responses(),
|
||||||
commands::utility::delete_auto_response(),
|
commands::utility::delete_auto_response(),
|
||||||
commands::utility::edit_auto_response(),
|
commands::utility::edit_auto_response(),
|
||||||
|
commands::utility::summary(),
|
||||||
],
|
],
|
||||||
prefix_options: poise::PrefixFrameworkOptions {
|
prefix_options: poise::PrefixFrameworkOptions {
|
||||||
prefix: Some("!".into()),
|
prefix: Some("!".into()),
|
||||||
@@ -80,7 +117,10 @@ async fn main() -> Result<(), Error> {
|
|||||||
.setup(move |context, _ready, framework| {
|
.setup(move |context, _ready, framework| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
poise::builtins::register_globally(context, &framework.options().commands).await?;
|
poise::builtins::register_globally(context, &framework.options().commands).await?;
|
||||||
Ok(Data { db: db_clone })
|
Ok(Data {
|
||||||
|
db: db_clone,
|
||||||
|
ai_chat: ai_chat_clone,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
@@ -94,11 +134,38 @@ async fn main() -> Result<(), Error> {
|
|||||||
let mut data = client.data.write().await;
|
let mut data = client.data.write().await;
|
||||||
data.insert::<commands::level::DbKey>(db.clone());
|
data.insert::<commands::level::DbKey>(db.clone());
|
||||||
data.insert::<commands::utility::DbKey>(db.clone());
|
data.insert::<commands::utility::DbKey>(db.clone());
|
||||||
|
data.insert::<commands::fun::AiChatKey>(ai_chat_manager.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get API port from environment, default to 8080
|
||||||
|
let api_port = env::var("API_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse::<u16>().ok())
|
||||||
|
.unwrap_or(8080);
|
||||||
|
|
||||||
|
// Run bot and API server concurrently
|
||||||
|
let cache = client.cache.clone();
|
||||||
|
let db_for_api = db.clone();
|
||||||
|
let bot_task = tokio::spawn(async move {
|
||||||
if let Err(why) = client.start_autosharded().await {
|
if let Err(why) = client.start_autosharded().await {
|
||||||
eprintln!("An error occurred while running the client: {why}");
|
eprintln!("An error occurred while running the client: {why}");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let api_task = api::start_api_server(cache, db_for_api, api_key, api_port);
|
||||||
|
|
||||||
|
// Wait for API to start, then run bot
|
||||||
|
tokio::select! {
|
||||||
|
_ = bot_task => {
|
||||||
|
info!("Bot task finished");
|
||||||
|
}
|
||||||
|
result = api_task => {
|
||||||
|
match result {
|
||||||
|
Ok(_) => info!("API server finished"),
|
||||||
|
Err(e) => eprintln!("API server error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user