Merge pull request 'migration to JS engine' (#1) from dev into main
Some checks failed
Build && Deploy / cargo build (push) Failing after 1m17s
Some checks failed
Build && Deploy / cargo build (push) Failing after 1m17s
Reviewed-on: #1
This commit is contained in:
commit
f7318f3661
@ -1,5 +1,8 @@
|
|||||||
name: Build && Deploy
|
name: Build && Deploy
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
189
Cargo.lock
generated
189
Cargo.lock
generated
@ -60,6 +60,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.98"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aquamarine"
|
name = "aquamarine"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@ -133,6 +139,26 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.71.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.0",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools 0.10.5",
|
||||||
|
"log",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
"shlex",
|
||||||
|
"syn 2.0.100",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@ -220,9 +246,20 @@ version = "1.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
|
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@ -265,6 +302,17 @@ dependencies = [
|
|||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -309,6 +357,15 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "copy_dir"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3"
|
||||||
|
dependencies = [
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@ -840,6 +897,12 @@ version = "0.31.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gongbotrs"
|
name = "gongbotrs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -853,10 +916,13 @@ dependencies = [
|
|||||||
"envconfig",
|
"envconfig",
|
||||||
"futures",
|
"futures",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"mongodb",
|
"mongodb",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
|
"quickjs-rusty",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"teloxide",
|
"teloxide",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1352,6 +1418,16 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.2",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.77"
|
version = "0.3.77"
|
||||||
@ -1362,12 +1438,39 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.171"
|
version = "0.2.171"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libquickjs-ng-sys"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c98c1ad542ec61348faba7ce5386fef9060e35fbeea19dda64ce41862084e0a"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cc",
|
||||||
|
"copy_dir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
@ -1502,6 +1605,12 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@ -1600,12 +1709,41 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@ -1828,6 +1966,16 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn 2.0.100",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-error-attr2"
|
name = "proc-macro-error-attr2"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -1858,6 +2006,22 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quickjs-rusty"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3b4d659d1bc37e9112a14ad9a7727d182b0fb12216eb6684bdbada3e9991a22"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"libquickjs-ng-sys",
|
||||||
|
"log",
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.40"
|
version = "1.0.40"
|
||||||
@ -2067,6 +2231,12 @@ version = "0.1.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -2157,6 +2327,15 @@ version = "1.0.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.27"
|
version = "0.1.27"
|
||||||
@ -3059,6 +3238,16 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|||||||
@ -15,10 +15,13 @@ enum_stringify = "0.6.3"
|
|||||||
envconfig = "0.11.0"
|
envconfig = "0.11.0"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
lazy_static = "1.5.0"
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
mongodb = "3.2.3"
|
mongodb = "3.2.3"
|
||||||
pretty_env_logger = "0.5.0"
|
pretty_env_logger = "0.5.0"
|
||||||
|
quickjs-rusty = "0.9.0"
|
||||||
serde = { version = "1.0.219", features = ["derive", "serde_derive"] }
|
serde = { version = "1.0.219", features = ["derive", "serde_derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
teloxide = { version = "0.14.0", features = ["macros", "postgres-storage-nativetls"] }
|
teloxide = { version = "0.14.0", features = ["macros", "postgres-storage-nativetls"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.44.1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.44.1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
|||||||
58
default_script.js
Normal file
58
default_script.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// db - is set globally
|
||||||
|
|
||||||
|
const dialog = {
|
||||||
|
commands: {
|
||||||
|
start: {
|
||||||
|
buttons: start_buttons, // default is `null`
|
||||||
|
state: "start"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
more_info: {},
|
||||||
|
},
|
||||||
|
stateful_msg_handlers: {
|
||||||
|
start: {}, // everything is by default, so just send message `start`
|
||||||
|
enter_name: {
|
||||||
|
// name of the handler function. This field has a side effect:
|
||||||
|
// when is set, no automatic sending of message, should be sent
|
||||||
|
// manually in handler
|
||||||
|
handler: enter_name,
|
||||||
|
state: "none"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function enter_name() {}
|
||||||
|
|
||||||
|
const fmt = (number) => number.toString().padStart(2, '0');
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
const [h, m, d, M, y] = [
|
||||||
|
date.getHours(),
|
||||||
|
date.getMinutes(),
|
||||||
|
date.getDate(),
|
||||||
|
date.getMonth(),
|
||||||
|
date.getFullYear()
|
||||||
|
];
|
||||||
|
return `${fmt(h)}:${fmt(m)} ${fmt(d)}-${fmt(M + 1)}-${y}`
|
||||||
|
};
|
||||||
|
|
||||||
|
function start_buttons() {
|
||||||
|
const now = new Date();
|
||||||
|
const dateFormated = formatDate(now);
|
||||||
|
|
||||||
|
// const user = db.find_one("users", {id: 1});
|
||||||
|
|
||||||
|
return [
|
||||||
|
// [{name: {name: user.first_name}, callback_name: "no"}],
|
||||||
|
[{name: {name: dateFormated}, callback_name: "no"}],
|
||||||
|
[{name: {name: "Hello!"}, callback_name: "no"}],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
version: 1.1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const c = {config: config, dialog: dialog}
|
||||||
|
c
|
||||||
82
mainbot.js
Normal file
82
mainbot.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// db - is set globally
|
||||||
|
|
||||||
|
const PROJECTS_COUNT = 2
|
||||||
|
|
||||||
|
const start_msg = {
|
||||||
|
buttons: [
|
||||||
|
[{ name: { literal: "show_projects" }, callback_name: "project_0" }],
|
||||||
|
[{ name: { literal: "more_info_btn" }, callback_name: "more_info" }],
|
||||||
|
[{ name: { literal: "leave_application" }, callback_name: "leave_application" }],
|
||||||
|
[{ name: { literal: "ask_question_btn" }, callback_name: "ask_question" }],
|
||||||
|
], // default is `null`
|
||||||
|
replace: true,
|
||||||
|
state: "start"
|
||||||
|
};
|
||||||
|
const dialog = {
|
||||||
|
commands: {
|
||||||
|
start: start_msg,
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
more_info: {
|
||||||
|
buttons: [
|
||||||
|
[{ name: { name: "На главную" }, callback_name: "start" }],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
start: start_msg,
|
||||||
|
leave_application: {
|
||||||
|
handler: leave_application
|
||||||
|
},
|
||||||
|
ask_question: {}
|
||||||
|
},
|
||||||
|
stateful_msg_handlers: {
|
||||||
|
start: {}, // everything is by default, so just send message `start`
|
||||||
|
enter_name: {
|
||||||
|
// name of the handler function. This field has a side effect:
|
||||||
|
// when is set, no automatic sending of message, should be sent
|
||||||
|
// manually in handler
|
||||||
|
handler: enter_name,
|
||||||
|
state: "none"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function leave_application(user) {
|
||||||
|
print(JSON.stringify(user))
|
||||||
|
user_application(user)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function enter_name() { }
|
||||||
|
|
||||||
|
const fmt = (number) => number.toString().padStart(2, '0');
|
||||||
|
|
||||||
|
function add_project_callbacks(point) {
|
||||||
|
for (const i of Array(PROJECTS_COUNT).keys()) {
|
||||||
|
buttons = [
|
||||||
|
[],
|
||||||
|
[{ name: { name: "На главную" }, callback_name: "start" }]
|
||||||
|
]
|
||||||
|
if (i > 0) {
|
||||||
|
buttons[0].push({ name: { literal: "prev_project" }, callback_name: `project_${i - 1}` })
|
||||||
|
}
|
||||||
|
if (i < PROJECTS_COUNT - 1) {
|
||||||
|
buttons[0].push({ name: { literal: "next_project" }, callback_name: `project_${i + 1}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
point[`project_${i}`] = {
|
||||||
|
replace: true,
|
||||||
|
buttons: buttons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_project_callbacks(dialog.buttons)
|
||||||
|
print(JSON.stringify(dialog.buttons))
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
version: 1.1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// {config, dialog}
|
||||||
|
const c = { config: config, dialog: dialog }
|
||||||
|
c
|
||||||
35
src/admin.rs
35
src/admin.rs
@ -5,7 +5,8 @@ use teloxide::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{CallDB, DB},
|
bot_manager::DEFAULT_SCRIPT,
|
||||||
|
db::{bots::BotInstance, CallDB, DB},
|
||||||
BotResult,
|
BotResult,
|
||||||
};
|
};
|
||||||
use crate::{BotDialogue, LogMsg, State};
|
use crate::{BotDialogue, LogMsg, State};
|
||||||
@ -41,6 +42,8 @@ pub enum AdminCommands {
|
|||||||
Users,
|
Users,
|
||||||
/// Cancel current action and sets user state to default
|
/// Cancel current action and sets user state to default
|
||||||
Cancel,
|
Cancel,
|
||||||
|
/// Create new instance of telegram bot
|
||||||
|
Deploy { token: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn admin_command_handler(
|
pub async fn admin_command_handler(
|
||||||
@ -156,6 +159,36 @@ pub async fn admin_command_handler(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
AdminCommands::Deploy { token } => {
|
||||||
|
let bot_instance = {
|
||||||
|
let botnew = Bot::new(&token);
|
||||||
|
let name = match botnew.get_me().await {
|
||||||
|
Ok(me) => me.username().to_string(),
|
||||||
|
Err(teloxide::RequestError::Api(teloxide::ApiError::InvalidToken)) => {
|
||||||
|
bot.send_message(msg.chat.id, "Error: bot token is invalid")
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let bi =
|
||||||
|
BotInstance::new(name.clone(), token.to_string(), DEFAULT_SCRIPT.to_string())
|
||||||
|
.store(&mut db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bi
|
||||||
|
};
|
||||||
|
|
||||||
|
bot.send_message(
|
||||||
|
msg.chat.id,
|
||||||
|
format!("Deployed bot with name: {}", bot_instance.name),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
229
src/bot_handler.rs
Normal file
229
src/bot_handler.rs
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
use log::{error, info};
|
||||||
|
use quickjs_rusty::serde::to_js;
|
||||||
|
use std::{
|
||||||
|
str::FromStr,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
use teloxide::{
|
||||||
|
dispatching::{dialogue::GetChatId, UpdateFilterExt},
|
||||||
|
dptree::{self, Handler},
|
||||||
|
prelude::DependencyMap,
|
||||||
|
types::{CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update},
|
||||||
|
Bot,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
botscript::{self, BotMessage, RunnerConfig},
|
||||||
|
commands::BotCommand,
|
||||||
|
db::{CallDB, DB},
|
||||||
|
message_answerer::MessageAnswerer,
|
||||||
|
update_user_tg, BotError, BotResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type BotHandler =
|
||||||
|
Handler<'static, DependencyMap, BotResult<()>, teloxide::dispatching::DpHandlerDescription>;
|
||||||
|
|
||||||
|
pub fn script_handler(rc: Arc<RwLock<RunnerConfig>>) -> BotHandler {
|
||||||
|
let crc = rc.clone();
|
||||||
|
dptree::entry()
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
// check if message is command
|
||||||
|
.filter_map(|m: Message| m.text().and_then(|t| BotCommand::from_str(t).ok()))
|
||||||
|
// check if command is presented in config
|
||||||
|
.filter_map(move |bc: BotCommand| {
|
||||||
|
let rc = std::sync::Arc::clone(&rc);
|
||||||
|
let command = bc.command();
|
||||||
|
|
||||||
|
let rc = rc.read().expect("RwLock lock on commands map failed");
|
||||||
|
|
||||||
|
rc.get_command_message(command)
|
||||||
|
})
|
||||||
|
.endpoint(handle_botmessage),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_callback_query()
|
||||||
|
.filter_map(move |q: CallbackQuery| {
|
||||||
|
q.data.and_then(|data| {
|
||||||
|
let rc = std::sync::Arc::clone(&crc);
|
||||||
|
let rc = rc.read().expect("RwLock lock on commands map failed");
|
||||||
|
|
||||||
|
rc.get_callback_message(&data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.endpoint(handle_callback),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_botmessage(bot: Bot, mut db: DB, bm: BotMessage, msg: Message) -> BotResult<()> {
|
||||||
|
info!("Eval BM: {:?}", bm);
|
||||||
|
let tguser = match msg.from.clone() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Ok(()), // do nothing, cause its not usecase of function
|
||||||
|
};
|
||||||
|
let user = db
|
||||||
|
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
||||||
|
.await?;
|
||||||
|
let user = update_user_tg(user, &tguser);
|
||||||
|
user.update_user(&mut db).await?;
|
||||||
|
|
||||||
|
let is_propagate: bool = match bm.get_handler() {
|
||||||
|
Some(handler) => 'prop: {
|
||||||
|
let ctx = match handler.context() {
|
||||||
|
Some(ctx) => ctx,
|
||||||
|
// falling back to propagation
|
||||||
|
None => break 'prop true,
|
||||||
|
};
|
||||||
|
let jsuser = to_js(ctx, &tguser).unwrap();
|
||||||
|
match handler.call_args(vec![jsuser]) {
|
||||||
|
Ok(v) => {
|
||||||
|
if v.is_bool() {
|
||||||
|
v.to_bool().unwrap_or(true)
|
||||||
|
} else if v.is_int() {
|
||||||
|
v.to_int().unwrap_or(1) != 0
|
||||||
|
} else {
|
||||||
|
// falling back to propagation
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to get return of handler, err: {err}");
|
||||||
|
// falling back to propagation
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_propagate {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttons = bm
|
||||||
|
.resolve_buttons(&mut db)
|
||||||
|
.await?
|
||||||
|
.map(|buttons| InlineKeyboardMarkup {
|
||||||
|
inline_keyboard: buttons
|
||||||
|
.iter()
|
||||||
|
.map(|r| {
|
||||||
|
r.iter()
|
||||||
|
.map(|b| match b {
|
||||||
|
botscript::ButtonLayout::Callback {
|
||||||
|
name,
|
||||||
|
literal: _,
|
||||||
|
callback,
|
||||||
|
} => InlineKeyboardButton::callback(name, callback),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
let literal = bm.literal().map_or("", |s| s.as_str());
|
||||||
|
|
||||||
|
let ma = MessageAnswerer::new(&bot, &mut db, msg.chat.id.0);
|
||||||
|
ma.answer(literal, None, buttons).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_callback(bot: Bot, mut db: DB, bm: BotMessage, q: CallbackQuery) -> BotResult<()> {
|
||||||
|
info!("Eval BM: {:?}", bm);
|
||||||
|
let tguser = q.from.clone();
|
||||||
|
let user = db
|
||||||
|
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
||||||
|
.await?;
|
||||||
|
let user = update_user_tg(user, &tguser);
|
||||||
|
user.update_user(&mut db).await?;
|
||||||
|
|
||||||
|
let is_propagate: bool = match bm.get_handler() {
|
||||||
|
Some(handler) => 'prop: {
|
||||||
|
let ctx = match handler.context() {
|
||||||
|
Some(ctx) => ctx,
|
||||||
|
// falling back to propagation
|
||||||
|
None => break 'prop true,
|
||||||
|
};
|
||||||
|
let jsuser = to_js(ctx, &tguser).unwrap();
|
||||||
|
match handler.call_args(vec![jsuser]) {
|
||||||
|
Ok(v) => {
|
||||||
|
if v.is_bool() {
|
||||||
|
v.to_bool().unwrap_or(true)
|
||||||
|
} else if v.is_int() {
|
||||||
|
v.to_int().unwrap_or(1) != 0
|
||||||
|
} else {
|
||||||
|
// falling back to propagation
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to get return of handler, err: {err}");
|
||||||
|
// falling back to propagation
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_propagate {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttons = bm
|
||||||
|
.resolve_buttons(&mut db)
|
||||||
|
.await?
|
||||||
|
.map(|buttons| InlineKeyboardMarkup {
|
||||||
|
inline_keyboard: buttons
|
||||||
|
.iter()
|
||||||
|
.map(|r| {
|
||||||
|
r.iter()
|
||||||
|
.map(|b| match b {
|
||||||
|
botscript::ButtonLayout::Callback {
|
||||||
|
name,
|
||||||
|
literal: _,
|
||||||
|
callback,
|
||||||
|
} => InlineKeyboardButton::callback(name, callback),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
let literal = bm.literal().map_or("", |s| s.as_str());
|
||||||
|
|
||||||
|
let (chat_id, msg_id) = {
|
||||||
|
let chat_id = match q.chat_id() {
|
||||||
|
Some(chat_id) => chat_id.0,
|
||||||
|
None => tguser.id.0 as i64,
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg_id = q.message.map_or_else(
|
||||||
|
|| {
|
||||||
|
Err(BotError::MsgTooOld(
|
||||||
|
"Failed to get message id, probably message too old".to_string(),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
|m| Ok(m.id().0),
|
||||||
|
);
|
||||||
|
|
||||||
|
(chat_id, msg_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
let ma = MessageAnswerer::new(&bot, &mut db, chat_id);
|
||||||
|
match bm.is_replace() {
|
||||||
|
true => {
|
||||||
|
match msg_id {
|
||||||
|
Ok(msg_id) => {
|
||||||
|
ma.replace_message(msg_id, literal, buttons).await?;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
ma.answer(literal, None, buttons).await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
ma.answer(literal, None, buttons).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
240
src/bot_manager.rs
Normal file
240
src/bot_manager.rs
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
use std::{collections::HashMap, future::Future, sync::RwLock, thread::JoinHandle, time::Duration};
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use log::{error, info};
|
||||||
|
use teloxide::{
|
||||||
|
dispatching::dialogue::serializer::Json,
|
||||||
|
dptree,
|
||||||
|
prelude::{Dispatcher, Requester},
|
||||||
|
Bot,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bot_handler::{script_handler, BotHandler},
|
||||||
|
db::{bots::BotInstance, DbError, DB},
|
||||||
|
mongodb_storage::MongodbStorage,
|
||||||
|
BotController, BotError, BotResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct BotRunner {
|
||||||
|
controller: BotController,
|
||||||
|
info: BotInfo,
|
||||||
|
thread: Option<JoinHandle<BotResult<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Sync for BotRunner {}
|
||||||
|
unsafe impl Send for BotRunner {}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BotInfo {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref BOT_POOL: RwLock<HashMap<String, BotRunner>> = RwLock::new(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static DEFAULT_SCRIPT: &str =
|
||||||
|
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/default_script.js"));
|
||||||
|
|
||||||
|
pub struct BotManager<BIG, HM, BIS, HI, FBIS, FHI>
|
||||||
|
where
|
||||||
|
BIG: FnMut() -> FBIS,
|
||||||
|
FBIS: Future<Output = BIS>,
|
||||||
|
BIS: Iterator<Item = BotInstance>,
|
||||||
|
HM: FnMut(BotInstance) -> FHI,
|
||||||
|
FHI: Future<Output = HI>,
|
||||||
|
HI: Iterator<Item = BotHandler>,
|
||||||
|
{
|
||||||
|
bot_pool: HashMap<String, BotRunner>,
|
||||||
|
bi_getter: BIG,
|
||||||
|
h_mapper: HM,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<BIG, HM, BIS, HI, FBIS, FHI> BotManager<BIG, HM, BIS, HI, FBIS, FHI>
|
||||||
|
where
|
||||||
|
BIG: FnMut() -> FBIS,
|
||||||
|
FBIS: Future<Output = BIS>,
|
||||||
|
BIS: Iterator<Item = BotInstance>,
|
||||||
|
HM: FnMut(BotInstance) -> FHI,
|
||||||
|
FHI: Future<Output = HI>,
|
||||||
|
HI: Iterator<Item = BotHandler>,
|
||||||
|
{
|
||||||
|
/// bi_getter - fnmut that returns iterator over BotInstance
|
||||||
|
/// h_map - fnmut that returns iterator over handlers by BotInstance
|
||||||
|
pub fn with(bi_getter: BIG, h_mapper: HM) -> Self {
|
||||||
|
Self {
|
||||||
|
bot_pool: Default::default(),
|
||||||
|
bi_getter,
|
||||||
|
h_mapper,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dispatch(mut self, db: &mut DB) -> ! {
|
||||||
|
loop {
|
||||||
|
'biter: for bi in (self.bi_getter)().await {
|
||||||
|
// removing handler to force restart
|
||||||
|
// TODO: wait till all updates are processed in bot
|
||||||
|
// Temporarly disabling code, because it's free of js runtime
|
||||||
|
// spreads panic
|
||||||
|
if bi.restart_flag {
|
||||||
|
info!(
|
||||||
|
"Trying to restart bot `{}`, new script: {}",
|
||||||
|
bi.name, bi.script
|
||||||
|
);
|
||||||
|
let runner = self.bot_pool.remove(&bi.name);
|
||||||
|
};
|
||||||
|
// start, if not started
|
||||||
|
let mut bot_runner = match self.bot_pool.remove(&bi.name) {
|
||||||
|
Some(br) => br,
|
||||||
|
None => {
|
||||||
|
let handlers = (self.h_mapper)(bi.clone()).await;
|
||||||
|
info!("NEW INSTANCE: Starting new instance! bot name: {}", bi.name);
|
||||||
|
self.start_bot(bi, db, handlers.collect()).await.unwrap();
|
||||||
|
continue 'biter;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// checking if thread is not finished, otherwise clearing handler
|
||||||
|
bot_runner.thread = match bot_runner.thread {
|
||||||
|
Some(thread) => {
|
||||||
|
if thread.is_finished() {
|
||||||
|
let err = thread.join();
|
||||||
|
error!("Thread bot `{}` finished with error: {:?}", bi.name, err);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(thread)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// checking if thread is running, otherwise start thread
|
||||||
|
bot_runner.thread = match bot_runner.thread {
|
||||||
|
Some(thread) => Some(thread),
|
||||||
|
None => {
|
||||||
|
let handlers = (self.h_mapper)(bi.clone()).await;
|
||||||
|
let handler =
|
||||||
|
script_handler_gen(bot_runner.controller.clone(), handlers.collect())
|
||||||
|
.await;
|
||||||
|
Some(
|
||||||
|
spawn_bot_thread(bot_runner.controller.clone(), db, handler)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.bot_pool.insert(bi.name.clone(), bot_runner);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_bot(
|
||||||
|
&mut self,
|
||||||
|
bi: BotInstance,
|
||||||
|
db: &mut DB,
|
||||||
|
plug_handlers: Vec<BotHandler>,
|
||||||
|
) -> BotResult<BotInfo> {
|
||||||
|
let mut db = db.clone().with_name(bi.name.clone());
|
||||||
|
let controller = BotController::with_db(db.clone(), &bi.token, &bi.script).await?;
|
||||||
|
|
||||||
|
let handler = script_handler_gen(controller.clone(), plug_handlers).await;
|
||||||
|
|
||||||
|
let thread = spawn_bot_thread(controller.clone(), &mut db, handler).await?;
|
||||||
|
|
||||||
|
let info = BotInfo {
|
||||||
|
name: bi.name.clone(),
|
||||||
|
};
|
||||||
|
let runner = BotRunner {
|
||||||
|
controller,
|
||||||
|
info: info.clone(),
|
||||||
|
thread: Some(thread),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.bot_pool.insert(bi.name.clone(), runner);
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn script_handler_gen(c: BotController, plug_handlers: Vec<BotHandler>) -> BotHandler {
|
||||||
|
let handler = script_handler(c.rc.clone());
|
||||||
|
// each handler will be added to dptree::entry()
|
||||||
|
let handler = plug_handlers
|
||||||
|
.into_iter()
|
||||||
|
// as well as the script handler at the end
|
||||||
|
.chain(std::iter::once(handler))
|
||||||
|
.fold(dptree::entry(), |h, plug| h.branch(plug));
|
||||||
|
handler
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_bot(
|
||||||
|
bi: BotInstance,
|
||||||
|
db: &mut DB,
|
||||||
|
plug_handlers: Vec<BotHandler>,
|
||||||
|
) -> BotResult<BotInfo> {
|
||||||
|
let mut db = db.clone().with_name(bi.name.clone());
|
||||||
|
let controller = BotController::with_db(db.clone(), &bi.token, &bi.script).await?;
|
||||||
|
|
||||||
|
let handler = script_handler(controller.rc.clone());
|
||||||
|
// each handler will be added to dptree::entry()
|
||||||
|
let handler = plug_handlers
|
||||||
|
.into_iter()
|
||||||
|
// as well as the script handler at the end
|
||||||
|
.chain(std::iter::once(handler))
|
||||||
|
.fold(dptree::entry(), |h, plug| h.branch(plug));
|
||||||
|
|
||||||
|
let thread = spawn_bot_thread(controller.clone(), &mut db, handler).await?;
|
||||||
|
|
||||||
|
let info = BotInfo {
|
||||||
|
name: bi.name.clone(),
|
||||||
|
};
|
||||||
|
let runner = BotRunner {
|
||||||
|
controller,
|
||||||
|
info: info.clone(),
|
||||||
|
thread: Some(thread),
|
||||||
|
};
|
||||||
|
|
||||||
|
BOT_POOL
|
||||||
|
.write()
|
||||||
|
.map_or_else(
|
||||||
|
|err| {
|
||||||
|
Err(BotError::RwLockError(format!(
|
||||||
|
"Failed to lock BOT_POOL because previous thread paniced, err: {err}"
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
Ok,
|
||||||
|
)?
|
||||||
|
.insert(bi.name.clone(), runner);
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spawn_bot_thread(
|
||||||
|
bc: BotController,
|
||||||
|
db: &mut DB,
|
||||||
|
handler: BotHandler,
|
||||||
|
) -> BotResult<JoinHandle<BotResult<()>>> {
|
||||||
|
let state_mgr = MongodbStorage::from_db(db, Json)
|
||||||
|
.await
|
||||||
|
.map_err(DbError::from)?;
|
||||||
|
let thread = std::thread::spawn(move || -> BotResult<()> {
|
||||||
|
let state_mgr = state_mgr;
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
rt.block_on(
|
||||||
|
Dispatcher::builder(bc.bot, handler)
|
||||||
|
.dependencies(dptree::deps![bc.db, state_mgr])
|
||||||
|
.build()
|
||||||
|
.dispatch(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(thread)
|
||||||
|
}
|
||||||
788
src/botscript.rs
Normal file
788
src/botscript.rs
Normal file
@ -0,0 +1,788 @@
|
|||||||
|
pub mod application;
|
||||||
|
pub mod db;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex, PoisonError};
|
||||||
|
|
||||||
|
use crate::db::raw_calls::RawCallError;
|
||||||
|
use crate::db::{CallDB, DbError, DB};
|
||||||
|
use crate::utils::parcelable::{ParcelType, Parcelable, ParcelableError, ParcelableResult};
|
||||||
|
use db::attach_db_obj;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use futures::lock::MutexGuard;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use quickjs_rusty::serde::from_js;
|
||||||
|
use quickjs_rusty::utils::create_empty_object;
|
||||||
|
use quickjs_rusty::utils::create_string;
|
||||||
|
use quickjs_rusty::ContextError;
|
||||||
|
use quickjs_rusty::ExecutionError;
|
||||||
|
use quickjs_rusty::JsFunction;
|
||||||
|
use quickjs_rusty::OwnedJsValue as JsValue;
|
||||||
|
use quickjs_rusty::ValueError;
|
||||||
|
use quickjs_rusty::{Context, OwnedJsObject};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ScriptError {
|
||||||
|
#[error("error context: {0:?}")]
|
||||||
|
ContextError(#[from] ContextError),
|
||||||
|
#[error("error running: {0:?}")]
|
||||||
|
ExecutionError(#[from] ExecutionError),
|
||||||
|
#[error("error from anyhow: {0:?}")]
|
||||||
|
SerdeError(#[from] quickjs_rusty::serde::Error),
|
||||||
|
#[error("error value: {0:?}")]
|
||||||
|
ValueError(#[from] ValueError),
|
||||||
|
#[error("error bot function execution: {0:?}")]
|
||||||
|
BotFunctionError(String),
|
||||||
|
#[error("error from DB: {0:?}")]
|
||||||
|
DBError(#[from] DbError),
|
||||||
|
#[error("error resolving data: {0:?}")]
|
||||||
|
ResolveError(#[from] ResolveError),
|
||||||
|
#[error("error while calling db from runtime: {0:?}")]
|
||||||
|
RawCallError(#[from] RawCallError),
|
||||||
|
#[error("error while locking mutex: {0:?}")]
|
||||||
|
MutexError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ResolveError {
|
||||||
|
#[error("wrong literal: {0:?}")]
|
||||||
|
IncorrectLiteral(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ScriptResult<T> = Result<T, ScriptError>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct BotFunction {
|
||||||
|
func: FunctionMarker,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum FunctionMarker {
|
||||||
|
/// serde is not able to (de)serialize this, so ignore it and fill
|
||||||
|
/// in runtime with injection in DeserializeJS
|
||||||
|
#[serde(skip)]
|
||||||
|
Function(JsFunction),
|
||||||
|
StrTemplate(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FunctionMarker {
|
||||||
|
pub fn as_str_template(&self) -> Option<&String> {
|
||||||
|
if let Self::StrTemplate(v) = self {
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_function(&self) -> Option<&JsFunction> {
|
||||||
|
if let Self::Function(v) = self {
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_js_function(&mut self, f: JsFunction) {
|
||||||
|
*self = Self::Function(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parcelable<Self> for BotFunction {
|
||||||
|
fn get_field(
|
||||||
|
&mut self,
|
||||||
|
_name: &str,
|
||||||
|
) -> crate::utils::parcelable::ParcelableResult<ParcelType<Self>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&mut self) -> ParcelableResult<ParcelType<Self>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
Ok(ParcelType::Function(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotFunction {
|
||||||
|
pub fn by_name(name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
func: FunctionMarker::StrTemplate(name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call_context(&self, runner: &Runner) -> ScriptResult<JsValue> {
|
||||||
|
match &self.func {
|
||||||
|
FunctionMarker::Function(f) => {
|
||||||
|
let val = f.call(Default::default())?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
FunctionMarker::StrTemplate(func_name) => runner.run_script(&format!("{func_name}()")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context(&self) -> Option<*mut quickjs_rusty::JSContext> {
|
||||||
|
match &self.func {
|
||||||
|
FunctionMarker::Function(js_function) => Some(js_function.context()),
|
||||||
|
FunctionMarker::StrTemplate(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call(&self) -> ScriptResult<JsValue> {
|
||||||
|
self.call_args(Default::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call_args(&self, args: Vec<JsValue>) -> ScriptResult<JsValue> {
|
||||||
|
if let FunctionMarker::Function(f) = &self.func {
|
||||||
|
let val = f.call(args)?;
|
||||||
|
Ok(val)
|
||||||
|
} else {
|
||||||
|
Err(ScriptError::BotFunctionError(
|
||||||
|
"Js Function is not defined".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_js_function(&mut self, f: JsFunction) {
|
||||||
|
self.func.set_js_function(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DeserializeJS {
|
||||||
|
fn js_into<'a, T: Deserialize<'a>>(&'a self) -> ScriptResult<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeserializeJS for JsValue {
|
||||||
|
fn js_into<'a, T: Deserialize<'a>>(&'a self) -> ScriptResult<T> {
|
||||||
|
let rc = from_js(self.context(), self)?;
|
||||||
|
|
||||||
|
Ok(rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DeserializerJS {
|
||||||
|
fn_map: HashMap<String, JsFunction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeserializerJS {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
fn_map: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize_js<'a, T: Deserialize<'a> + Parcelable<BotFunction> + 'static>(
|
||||||
|
value: &'a JsValue,
|
||||||
|
) -> ScriptResult<T> {
|
||||||
|
let mut s = Self::new();
|
||||||
|
|
||||||
|
s.inject_templates(value, "".to_string())?;
|
||||||
|
|
||||||
|
let mut res = value.js_into()?;
|
||||||
|
|
||||||
|
for (k, jsf) in s.fn_map {
|
||||||
|
let item: ParcelType<'_, BotFunction> =
|
||||||
|
match Parcelable::<BotFunction>::get_nested(&mut res, &k) {
|
||||||
|
Ok(item) => item,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to inject original functions to structs, error: {err}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let ParcelType::Function(f) = item {
|
||||||
|
f.set_js_function(jsf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inject_templates(
|
||||||
|
&mut self,
|
||||||
|
value: &JsValue,
|
||||||
|
path: String,
|
||||||
|
) -> ScriptResult<Option<String>> {
|
||||||
|
if let Ok(f) = value.clone().try_into_function() {
|
||||||
|
self.fn_map.insert(path.clone(), f);
|
||||||
|
return Ok(Some(path));
|
||||||
|
} else if let Ok(o) = value.clone().try_into_object() {
|
||||||
|
let path = if path.is_empty() { path } else { path + "." }; // trying to avoid . in the start
|
||||||
|
// of stringified path
|
||||||
|
let res = o
|
||||||
|
.properties_iter()?
|
||||||
|
.chunks(2)
|
||||||
|
.into_iter()
|
||||||
|
// since chunks(2) is used and properties iterator over object
|
||||||
|
// always has even elements, unwrap will not fail
|
||||||
|
.map(
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
|mut chunk| (chunk.next().unwrap(), chunk.next().unwrap()),
|
||||||
|
)
|
||||||
|
.map(|(k, p)| k.and_then(|k| p.map(|p| (k, p))))
|
||||||
|
.filter_map(|m| m.ok())
|
||||||
|
.try_for_each(|(k, p)| {
|
||||||
|
let k = match k.to_string() {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(err) => return Err(ScriptError::ValueError(err)),
|
||||||
|
};
|
||||||
|
let res = match self.inject_templates(&p, path.clone() + &k)? {
|
||||||
|
Some(_) => {
|
||||||
|
let fo = JsValue::new(
|
||||||
|
o.context(),
|
||||||
|
create_empty_object(o.context()).expect("couldn't create object"),
|
||||||
|
)
|
||||||
|
.try_into_object()
|
||||||
|
.expect("the object created was not an object :/");
|
||||||
|
fo.set_property(
|
||||||
|
"func",
|
||||||
|
JsValue::new(
|
||||||
|
o.context(),
|
||||||
|
create_string(o.context(), "somefunc")
|
||||||
|
.expect("couldn't create string"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("wasn't able to set property on object :/");
|
||||||
|
o.set_property(&k, fo.into_value())
|
||||||
|
}
|
||||||
|
None => Ok(()),
|
||||||
|
};
|
||||||
|
match res {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(ScriptError::ExecutionError(err)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res?;
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove this function since it is suitable only for early development
|
||||||
|
#[allow(clippy::print_stdout)]
|
||||||
|
fn print(s: String) {
|
||||||
|
println!("{s}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct BotConfig {
|
||||||
|
version: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ResolveValue {
|
||||||
|
type Value;
|
||||||
|
|
||||||
|
fn resolve(self) -> ScriptResult<Self::Value>;
|
||||||
|
fn resolve_with(self, runner: &Runner) -> ScriptResult<Self::Value>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum KeyboardDefinition {
|
||||||
|
Rows(Vec<RowDefinition>),
|
||||||
|
Function(BotFunction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parcelable<BotFunction> for KeyboardDefinition {
|
||||||
|
fn get_field(&mut self, _name: &str) -> ParcelableResult<ParcelType<BotFunction>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn resolve(&mut self) -> ParcelableResult<ParcelType<BotFunction>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
KeyboardDefinition::Rows(rows) => Ok(rows.resolve()?),
|
||||||
|
KeyboardDefinition::Function(f) => Ok(f.resolve()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolveValue for KeyboardDefinition {
|
||||||
|
type Value = Vec<<RowDefinition as ResolveValue>::Value>;
|
||||||
|
|
||||||
|
fn resolve(self) -> ScriptResult<Self::Value> {
|
||||||
|
match self {
|
||||||
|
KeyboardDefinition::Rows(rows) => rows.into_iter().map(|r| r.resolve()).collect(),
|
||||||
|
KeyboardDefinition::Function(f) => {
|
||||||
|
<Self as ResolveValue>::resolve(f.call()?.js_into()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_with(self, runner: &Runner) -> ScriptResult<Self::Value> {
|
||||||
|
match self {
|
||||||
|
KeyboardDefinition::Rows(rows) => {
|
||||||
|
rows.into_iter().map(|r| r.resolve_with(runner)).collect()
|
||||||
|
}
|
||||||
|
KeyboardDefinition::Function(f) => {
|
||||||
|
<Self as ResolveValue>::resolve_with(f.call_context(runner)?.js_into()?, runner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum RowDefinition {
|
||||||
|
Buttons(Vec<ButtonDefinition>),
|
||||||
|
Function(BotFunction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parcelable<BotFunction> for RowDefinition {
|
||||||
|
fn get_field(&mut self, _name: &str) -> ParcelableResult<ParcelType<BotFunction>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn resolve(&mut self) -> ParcelableResult<ParcelType<BotFunction>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Buttons(buttons) => Ok(buttons.resolve()?),
|
||||||
|
Self::Function(f) => Ok(f.resolve()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolveValue for RowDefinition {
|
||||||
|
type Value = Vec<<ButtonDefinition as ResolveValue>::Value>;
|
||||||
|
|
||||||
|
fn resolve(self) -> ScriptResult<Self::Value> {
|
||||||
|
match self {
|
||||||
|
RowDefinition::Buttons(buttons) => buttons.into_iter().map(|b| b.resolve()).collect(),
|
||||||
|
RowDefinition::Function(f) => <Self as ResolveValue>::resolve(f.call()?.js_into()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_with(self, runner: &Runner) -> ScriptResult<Self::Value> {
|
||||||
|
match self {
|
||||||
|
RowDefinition::Buttons(buttons) => buttons
|
||||||
|
.into_iter()
|
||||||
|
.map(|b| b.resolve_with(runner))
|
||||||
|
.collect(),
|
||||||
|
RowDefinition::Function(f) => {
|
||||||
|
<Self as ResolveValue>::resolve_with(f.call_context(runner)?.js_into()?, runner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ButtonDefinition {
|
||||||
|
Button(ButtonRaw),
|
||||||
|
ButtonLiteral(String),
|
||||||
|
Function(BotFunction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolveValue for ButtonDefinition {
|
||||||
|
type Value = ButtonRaw;
|
||||||
|
|
||||||
|
fn resolve(self) -> ScriptResult<Self::Value> {
|
||||||
|
match self {
|
||||||
|
ButtonDefinition::Button(button) => Ok(button),
|
||||||
|
ButtonDefinition::ButtonLiteral(l) => Ok(ButtonRaw::from_literal(l)),
|
||||||
|
ButtonDefinition::Function(f) => <Self as ResolveValue>::resolve(f.call()?.js_into()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_with(self, runner: &Runner) -> ScriptResult<Self::Value> {
|
||||||
|
match self {
|
||||||
|
ButtonDefinition::Button(button) => Ok(button),
|
||||||
|
ButtonDefinition::ButtonLiteral(l) => Ok(ButtonRaw::from_literal(l)),
|
||||||
|
ButtonDefinition::Function(f) => {
|
||||||
|
<Self as ResolveValue>::resolve_with(f.call_context(runner)?.js_into()?, runner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parcelable<BotFunction> for ButtonDefinition {
|
||||||
|
fn get_field(&mut self, _name: &str) -> ParcelableResult<ParcelType<BotFunction>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
fn resolve(&mut self) -> ParcelableResult<ParcelType<BotFunction>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Button(braw) => Ok(braw.resolve()?),
|
||||||
|
Self::ButtonLiteral(s) => Ok(s.resolve()?),
|
||||||
|
Self::Function(f) => Ok(f.resolve()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct ButtonRaw {
|
||||||
|
name: ButtonName,
|
||||||
|
callback_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F> Parcelable<F> for ButtonRaw {
|
||||||
|
fn get_field(&mut self, _name: &str) -> ParcelableResult<ParcelType<F>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonRaw {
|
||||||
|
pub fn from_literal(literal: String) -> Self {
|
||||||
|
ButtonRaw {
|
||||||
|
name: ButtonName::Literal {
|
||||||
|
literal: literal.clone(),
|
||||||
|
},
|
||||||
|
callback_name: literal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &ButtonName {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn callback_name(&self) -> &str {
|
||||||
|
&self.callback_name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn literal(&self) -> Option<String> {
|
||||||
|
match self.name() {
|
||||||
|
ButtonName::Value { .. } => None,
|
||||||
|
ButtonName::Literal { literal } => Some(literal.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ButtonName {
|
||||||
|
Value { name: String },
|
||||||
|
Literal { literal: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonName {
|
||||||
|
pub async fn resolve_name(self, db: &mut DB) -> ScriptResult<String> {
|
||||||
|
match self {
|
||||||
|
ButtonName::Value { name } => Ok(name),
|
||||||
|
ButtonName::Literal { literal } => {
|
||||||
|
let value = db.get_literal_value(&literal).await?;
|
||||||
|
|
||||||
|
Ok(match value {
|
||||||
|
Some(value) => Ok(value),
|
||||||
|
None => Err(ResolveError::IncorrectLiteral(format!(
|
||||||
|
"not found literal `{literal}` in DB"
|
||||||
|
))),
|
||||||
|
}?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct Button {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct BotMessage {
|
||||||
|
// buttons: Vec<Button>
|
||||||
|
literal: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
replace: bool,
|
||||||
|
buttons: Option<KeyboardDefinition>,
|
||||||
|
state: Option<String>,
|
||||||
|
|
||||||
|
handler: Option<BotFunction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotMessage {
|
||||||
|
pub fn fill_literal(&self, l: String) -> Self {
|
||||||
|
BotMessage {
|
||||||
|
literal: self.clone().literal.or(Some(l)),
|
||||||
|
..self.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_replace(&self) -> bool {
|
||||||
|
self.replace
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_handler(&self) -> Option<&BotFunction> {
|
||||||
|
self.handler.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotMessage {
|
||||||
|
pub async fn resolve_buttons(
|
||||||
|
&self,
|
||||||
|
db: &mut DB,
|
||||||
|
) -> ScriptResult<Option<Vec<Vec<ButtonLayout>>>> {
|
||||||
|
let raw_buttons = self.buttons.clone().map(|b| b.resolve()).transpose()?;
|
||||||
|
match raw_buttons {
|
||||||
|
Some(braws) => {
|
||||||
|
let kbd: Vec<Vec<_>> = join_all(braws.into_iter().map(|rows| async {
|
||||||
|
join_all(rows.into_iter().map(|b| async {
|
||||||
|
let mut db = db.clone();
|
||||||
|
ButtonLayout::resolve_raw(b, &mut db).await
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
Ok(Some(kbd))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn literal(&self) -> Option<&String> {
|
||||||
|
self.literal.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ButtonLayout {
|
||||||
|
Callback {
|
||||||
|
name: String,
|
||||||
|
literal: Option<String>,
|
||||||
|
callback: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonLayout {
|
||||||
|
pub async fn resolve_raw(braw: ButtonRaw, db: &mut DB) -> ScriptResult<Self> {
|
||||||
|
let name = braw.name().clone().resolve_name(db).await?;
|
||||||
|
let literal = braw.literal();
|
||||||
|
let callback = braw.callback_name().to_string();
|
||||||
|
Ok(Self::Callback {
|
||||||
|
name,
|
||||||
|
literal,
|
||||||
|
callback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parcelable<BotFunction> for BotMessage {
|
||||||
|
fn get_field(&mut self, name: &str) -> ParcelableResult<ParcelType<BotFunction>> {
|
||||||
|
match name {
|
||||||
|
"buttons" => Ok(self.buttons.resolve()?),
|
||||||
|
"state" => Ok(self.state.resolve()?),
|
||||||
|
"handler" => Ok(self.handler.resolve()?),
|
||||||
|
field => Err(ParcelableError::FieldError(format!(
|
||||||
|
"tried to get field {field}, but this field does not exists or private"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct BotDialog {
|
||||||
|
pub commands: HashMap<String, BotMessage>,
|
||||||
|
pub buttons: HashMap<String, BotMessage>,
|
||||||
|
stateful_msg_handlers: HashMap<String, BotMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parcelable<BotFunction> for BotDialog {
|
||||||
|
fn get_field(&mut self, name: &str) -> Result<ParcelType<BotFunction>, ParcelableError> {
|
||||||
|
match name {
|
||||||
|
"commands" => Ok(ParcelType::Parcelable(&mut self.commands)),
|
||||||
|
"buttons" => Ok(ParcelType::Parcelable(&mut self.buttons)),
|
||||||
|
"stateful_msg_handlers" => Ok(ParcelType::Parcelable(&mut self.stateful_msg_handlers)),
|
||||||
|
field => Err(ParcelableError::FieldError(format!(
|
||||||
|
"tried to get field {field}, but this field does not exists or private"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct RunnerConfig {
|
||||||
|
config: BotConfig,
|
||||||
|
pub dialog: BotDialog,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunnerConfig {
|
||||||
|
/// command without starting `/`
|
||||||
|
pub fn get_command_message(&self, command: &str) -> Option<BotMessage> {
|
||||||
|
let bm = self.dialog.commands.get(command).cloned();
|
||||||
|
|
||||||
|
bm.map(|bm| bm.fill_literal(command.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_callback_message(&self, callback: &str) -> Option<BotMessage> {
|
||||||
|
let bm = self.dialog.buttons.get(callback).cloned();
|
||||||
|
|
||||||
|
bm.map(|bm| bm.fill_literal(callback.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parcelable<BotFunction> for RunnerConfig {
|
||||||
|
fn get_field(&mut self, name: &str) -> Result<ParcelType<BotFunction>, ParcelableError> {
|
||||||
|
match name {
|
||||||
|
"dialog" => Ok(ParcelType::Parcelable(&mut self.dialog)),
|
||||||
|
field => Err(ParcelableError::FieldError(format!(
|
||||||
|
"tried to get field {field}, but this field does not exists or private"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Runner {
|
||||||
|
context: Arc<Mutex<Context>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runner {
|
||||||
|
pub fn init() -> ScriptResult<Self> {
|
||||||
|
let context = Context::new(None)?;
|
||||||
|
|
||||||
|
context.add_callback("print", |a: String| {
|
||||||
|
print(a);
|
||||||
|
|
||||||
|
None::<bool>
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Runner {
|
||||||
|
context: Arc::new(Mutex::new(context)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_with_db(db: &mut DB) -> ScriptResult<Self> {
|
||||||
|
let context = Context::new(None)?;
|
||||||
|
let mut global = context.global()?;
|
||||||
|
attach_db_obj(&context, &mut global, db)?;
|
||||||
|
|
||||||
|
context.add_callback("print", |a: String| {
|
||||||
|
print(a);
|
||||||
|
|
||||||
|
None::<bool>
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Runner {
|
||||||
|
context: Arc::new(Mutex::new(context)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call_attacher<F, R>(&mut self, f: F) -> ScriptResult<R>
|
||||||
|
where
|
||||||
|
F: FnOnce(&Context, &mut OwnedJsObject) -> R,
|
||||||
|
{
|
||||||
|
let context = self.context.lock().unwrap();
|
||||||
|
let mut global = context.global()?;
|
||||||
|
|
||||||
|
let res = f(&context, &mut global);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_script(&self, content: &str) -> ScriptResult<JsValue> {
|
||||||
|
let ctx = match self.context.lock() {
|
||||||
|
Ok(ctx) => ctx,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(ScriptError::MutexError(format!(
|
||||||
|
"can't lock js Context mutex, err: {err}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let val = ctx.eval(content, false)?;
|
||||||
|
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_config(&self, content: &str) -> ScriptResult<RunnerConfig> {
|
||||||
|
let val = self.run_script(content)?;
|
||||||
|
|
||||||
|
// let rc: RunnerConfig = from_js(unsafe { self.context.context_raw() }, &val)?;
|
||||||
|
let rc: RunnerConfig = DeserializerJS::deserialize_js(&val)?;
|
||||||
|
|
||||||
|
Ok(rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
// allowing this since it is better for debugging tests)
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
#[allow(clippy::print_stdout)]
|
||||||
|
mod tests {
|
||||||
|
use quickjs_rusty::{serde::from_js, OwnedJsObject};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_script_valid() {
|
||||||
|
let runner = Runner::init().unwrap();
|
||||||
|
let val = runner.run_script(r#"print"#).unwrap();
|
||||||
|
println!("Val: {:?}", val);
|
||||||
|
let val = runner.run_script(r#"print('Hello from JS!');"#).unwrap();
|
||||||
|
println!("Val: {:?}", val);
|
||||||
|
assert!(val.is_null());
|
||||||
|
let val = runner.run_script(r#"const a = 1+2; a"#).unwrap();
|
||||||
|
println!("Val: {:?}", val);
|
||||||
|
assert_eq!(val.to_int(), Ok(3));
|
||||||
|
let val = runner.run_script(r#"a + 39"#).unwrap();
|
||||||
|
println!("Val: {:?}", val);
|
||||||
|
assert_eq!(val.to_int(), Ok(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_script_file_main() {
|
||||||
|
let runner = Runner::init().unwrap();
|
||||||
|
let val = runner.run_script(include_str!("../mainbot.js")).unwrap();
|
||||||
|
println!("config: {:?}", val);
|
||||||
|
let d: RunnerConfig = DeserializerJS::deserialize_js(&val).unwrap();
|
||||||
|
println!("desr rc: {:?}", d);
|
||||||
|
let val = runner.run_script("start_buttons()").unwrap();
|
||||||
|
println!("Val: {:?}", val.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_func_deserialization_main() {
|
||||||
|
let runner = Runner::init().unwrap();
|
||||||
|
let _ = runner
|
||||||
|
.run_script("function cancel_buttons() {return 'cancelation'}")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let f = BotFunction::by_name("cancel_buttons".to_string());
|
||||||
|
let res = f.call_context(&runner).unwrap();
|
||||||
|
|
||||||
|
println!("RES: {res:?}");
|
||||||
|
let sres: String = res.js_into().unwrap();
|
||||||
|
println!("Deserialized RES: {:?}", sres);
|
||||||
|
assert_eq!(sres, "cancelation");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recursive_format(o: OwnedJsObject) -> String {
|
||||||
|
let props: Vec<_> = o.properties_iter().unwrap().map(|x| x.unwrap()).collect();
|
||||||
|
let sp: Vec<String> = props
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| {
|
||||||
|
if v.is_object() {
|
||||||
|
recursive_format(v.try_into_object().unwrap())
|
||||||
|
} else {
|
||||||
|
format!("{:?}", v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!("{:?}", sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_script_invalid() {
|
||||||
|
let runner = Runner::init().unwrap();
|
||||||
|
let result = runner.run_script(r#"invalid_script();"#);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let errstr =
|
||||||
|
if let Err(ScriptError::ExecutionError(ExecutionError::Exception(errstr))) = result {
|
||||||
|
errstr.to_string().unwrap()
|
||||||
|
} else {
|
||||||
|
panic!("test returned wrong error!, {result:?}");
|
||||||
|
};
|
||||||
|
if errstr != "ReferenceError: invalid_script is not defined" {
|
||||||
|
panic!("test returned an error, but the wrong one, {errstr}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/botscript/application.rs
Normal file
81
src/botscript/application.rs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use quickjs_rusty::{context::Context, serde::from_js, OwnedJsObject};
|
||||||
|
use teloxide::Bot;
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{application::Application, message_forward::MessageForward, CallDB, DB},
|
||||||
|
message_answerer::MessageAnswerer,
|
||||||
|
send_application_to_chat, BotError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ScriptError;
|
||||||
|
|
||||||
|
pub fn attach_user_application(
|
||||||
|
c: &Context,
|
||||||
|
o: &mut OwnedJsObject,
|
||||||
|
db: &DB,
|
||||||
|
bot: &Bot,
|
||||||
|
) -> Result<(), ScriptError> {
|
||||||
|
let db: std::sync::Arc<RwLock<DB>> = std::sync::Arc::new(RwLock::new(db.clone()));
|
||||||
|
let dbbox = Box::new(db.clone());
|
||||||
|
let db: &'static _ = Box::leak(dbbox);
|
||||||
|
|
||||||
|
let bot: std::sync::Arc<RwLock<Bot>> = std::sync::Arc::new(RwLock::new(bot.clone()));
|
||||||
|
let botbox = Box::new(bot.clone());
|
||||||
|
let bot: &'static _ = Box::leak(botbox);
|
||||||
|
|
||||||
|
let user_application =
|
||||||
|
c.create_callback(move |q: OwnedJsObject| -> Result<_, ScriptError> {
|
||||||
|
let db = db.clone();
|
||||||
|
let user: teloxide::types::User = match from_js(q.context(), &q) {
|
||||||
|
Ok(q) => q,
|
||||||
|
Err(_) => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let application = futures::executor::block_on(
|
||||||
|
Application::new(user.clone()).store_db(&mut db.write().unwrap()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let db2 = db.clone();
|
||||||
|
let msg = tokio::task::block_in_place(move || {
|
||||||
|
Handle::current().block_on(async move {
|
||||||
|
send_application_to_chat(
|
||||||
|
&bot.read().unwrap(),
|
||||||
|
&mut db2.write().unwrap(),
|
||||||
|
&application,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let msg = match msg {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(err) => {
|
||||||
|
info!("Got err: {err}");
|
||||||
|
return Err(ScriptError::MutexError("🤦♂️".to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (chat_id, msg_id) = futures::executor::block_on(
|
||||||
|
MessageAnswerer::new(
|
||||||
|
&bot.read().unwrap(),
|
||||||
|
&mut db.write().unwrap(),
|
||||||
|
user.id.0 as i64,
|
||||||
|
)
|
||||||
|
.answer("left_application_msg", None, None),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
futures::executor::block_on(
|
||||||
|
MessageForward::new(msg.chat.id.0, msg.id.0, chat_id, msg_id, false)
|
||||||
|
.store_db(&mut db.write().unwrap()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ret = true;
|
||||||
|
Ok(ret)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
o.set_property("user_application", user_application.into_value())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
50
src/botscript/db.rs
Normal file
50
src/botscript/db.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use quickjs_rusty::context::Context;
|
||||||
|
|
||||||
|
use quickjs_rusty::serde::{from_js, to_js};
|
||||||
|
use quickjs_rusty::{utils::create_empty_object, OwnedJsObject, OwnedJsValue as JsValue};
|
||||||
|
|
||||||
|
use crate::db::raw_calls::RawCall;
|
||||||
|
use crate::db::DB;
|
||||||
|
|
||||||
|
use super::ScriptError;
|
||||||
|
|
||||||
|
pub fn attach_db_obj(c: &Context, o: &mut OwnedJsObject, db: &DB) -> Result<(), ScriptError> {
|
||||||
|
let dbobj = JsValue::new(o.context(), create_empty_object(o.context())?)
|
||||||
|
.try_into_object()
|
||||||
|
.expect("the created object was not an object :/");
|
||||||
|
|
||||||
|
let db: std::sync::Arc<RwLock<DB>> = std::sync::Arc::new(RwLock::new(db.clone()));
|
||||||
|
let dbbox = Box::new(db);
|
||||||
|
let db: &'static _ = Box::leak(dbbox);
|
||||||
|
|
||||||
|
let find_one = c.create_callback(
|
||||||
|
|collection: String, q: OwnedJsObject| -> Result<_, ScriptError> {
|
||||||
|
let query: serde_json::Value = match from_js(q.context(), &q) {
|
||||||
|
Ok(q) => q,
|
||||||
|
Err(_) => todo!(),
|
||||||
|
};
|
||||||
|
let db = db.clone();
|
||||||
|
|
||||||
|
let value = futures::executor::block_on(
|
||||||
|
db.write()
|
||||||
|
.expect("failed to gain write acces to db (probably RwLock is poisoned)")
|
||||||
|
.find_one(&collection, query),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ret = match value {
|
||||||
|
Some(v) => Some(to_js(q.context(), &v)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
Ok(ret)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let find_one = JsValue::from((unsafe { c.context_raw() }, find_one));
|
||||||
|
|
||||||
|
dbobj.set_property("find_one", find_one)?;
|
||||||
|
|
||||||
|
o.set_property("db", dbobj.into_value())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
100
src/commands.rs
Normal file
100
src/commands.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use teloxide::utils::command::ParseError;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum CommandError {
|
||||||
|
#[error("parse error: {0:?}")]
|
||||||
|
ParseError(#[from] ParseError),
|
||||||
|
#[error("failed to validate command: {0:?}")]
|
||||||
|
ValidationError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BotCommand {
|
||||||
|
command: String,
|
||||||
|
args: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for BotCommand {
|
||||||
|
type Err = CommandError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (command, args) = s.split_once(" ").map_or((s, None), |s| (s.0, Some(s.1)));
|
||||||
|
|
||||||
|
match command.strip_prefix("/") {
|
||||||
|
Some(command) => Ok(Self {
|
||||||
|
command: command.to_string(),
|
||||||
|
args: args.map(str::to_string),
|
||||||
|
}),
|
||||||
|
None => Err(CommandError::ParseError(ParseError::IncorrectFormat(
|
||||||
|
"Not a command".into(),
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotCommand {
|
||||||
|
pub fn from_validate(s: &str, cmds: &[&str]) -> Result<Self, CommandError> {
|
||||||
|
let bc = Self::from_str(s)?;
|
||||||
|
|
||||||
|
if !cmds.contains(&bc.command.as_str()) {
|
||||||
|
return Err(CommandError::ValidationError(format!(
|
||||||
|
"invalid command {}",
|
||||||
|
bc.command
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(bc)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command(&self) -> &str {
|
||||||
|
&self.command
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn args(&self) -> Option<&str> {
|
||||||
|
self.args.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn args_list(&self) -> Vec<&str> {
|
||||||
|
let args = match self.args {
|
||||||
|
Some(ref args) => args.as_str(),
|
||||||
|
None => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
args.split_whitespace().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_botcommand_from_str_simple() {
|
||||||
|
let cmdstr = "/start";
|
||||||
|
|
||||||
|
let bc = BotCommand::from_str(cmdstr).unwrap();
|
||||||
|
assert_eq!(bc.command(), "start");
|
||||||
|
assert_eq!(bc.args(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_botcommand_from_str_with_args() {
|
||||||
|
let cmdstr = "/some_long_cmd arg1 arg2";
|
||||||
|
|
||||||
|
let bc = BotCommand::from_str(cmdstr).unwrap();
|
||||||
|
assert_eq!(bc.command(), "some_long_cmd");
|
||||||
|
assert_eq!(bc.args(), Some("arg1 arg2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_botcommand_arg_list() {
|
||||||
|
let cmdstr = "/some_long_cmd arg1 arg2";
|
||||||
|
|
||||||
|
let bc = BotCommand::from_str(cmdstr).unwrap();
|
||||||
|
assert_eq!(bc.command(), "some_long_cmd");
|
||||||
|
assert_eq!(bc.args(), Some("arg1 arg2"));
|
||||||
|
assert_eq!(bc.args_list(), vec!["arg1", "arg2"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ use chrono::{DateTime, FixedOffset, Local};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::DbResult;
|
use super::DbResult;
|
||||||
|
use super::DB;
|
||||||
use crate::query_call_consume;
|
use crate::query_call_consume;
|
||||||
use crate::CallDB;
|
use crate::CallDB;
|
||||||
|
|
||||||
@ -36,4 +37,13 @@ where
|
|||||||
|
|
||||||
Ok(self)
|
Ok(self)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub async fn store_db(self, db: &mut DB) -> DbResult<Self> {
|
||||||
|
let db = db.get_database().await;
|
||||||
|
let ci = db.collection::<Self>("applications");
|
||||||
|
|
||||||
|
ci.insert_one(&self).await?;
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
93
src/db/bots.rs
Normal file
93
src/db/bots.rs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
use bson::doc;
|
||||||
|
use bson::oid::ObjectId;
|
||||||
|
use chrono::{DateTime, FixedOffset, Local};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::DbCollection;
|
||||||
|
use super::DbResult;
|
||||||
|
use crate::db::GetCollection;
|
||||||
|
use crate::query_call_consume;
|
||||||
|
use crate::CallDB;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||||
|
pub struct BotInstance {
|
||||||
|
pub _id: bson::oid::ObjectId,
|
||||||
|
pub name: String,
|
||||||
|
pub token: String,
|
||||||
|
pub script: String,
|
||||||
|
pub restart_flag: bool,
|
||||||
|
pub created_at: DateTime<FixedOffset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbCollection for BotInstance {
|
||||||
|
const COLLECTION: &str = "bots";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotInstance {
|
||||||
|
pub fn new(name: String, token: String, script: String) -> Self {
|
||||||
|
Self {
|
||||||
|
_id: Default::default(),
|
||||||
|
name,
|
||||||
|
token,
|
||||||
|
script,
|
||||||
|
restart_flag: false,
|
||||||
|
created_at: Local::now().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query_call_consume!(store, self, db, Self, {
|
||||||
|
let bi = db.get_collection::<Self>().await;
|
||||||
|
|
||||||
|
bi.insert_one(&self).await?;
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
});
|
||||||
|
|
||||||
|
pub async fn get_all<D: CallDB>(db: &mut D) -> DbResult<Vec<Self>> {
|
||||||
|
let bi = db.get_collection::<Self>().await;
|
||||||
|
|
||||||
|
Ok(bi.find(doc! {}).await?.try_collect().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_name<D: CallDB>(db: &mut D, name: &str) -> DbResult<Option<Self>> {
|
||||||
|
let bi = db.get_collection::<Self>().await;
|
||||||
|
|
||||||
|
Ok(bi.find_one(doc! {"name": name}).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restart_one<D: CallDB>(db: &mut D, name: &str, restart: bool) -> DbResult<()> {
|
||||||
|
let bi = db.get_collection::<Self>().await;
|
||||||
|
|
||||||
|
bi.update_one(
|
||||||
|
doc! {"name": name},
|
||||||
|
doc! { "$set": { "restart_flag": restart } },
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restart_all<D: CallDB>(db: &mut D, restart: bool) -> DbResult<()> {
|
||||||
|
let bi = db.get_collection::<Self>().await;
|
||||||
|
|
||||||
|
bi.update_many(doc! {}, doc! { "$set": { "restart_flag": restart } })
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_script<D: CallDB>(db: &mut D, name: &str, script: &str) -> DbResult<()> {
|
||||||
|
let bi = db.get_collection::<Self>().await;
|
||||||
|
|
||||||
|
bi.update_one(
|
||||||
|
doc! {"name": name},
|
||||||
|
doc! { "$set": {
|
||||||
|
"script": script,
|
||||||
|
"restart_flag": true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ use bson::doc;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::DbResult;
|
use super::DbResult;
|
||||||
|
use super::DB;
|
||||||
use crate::query_call_consume;
|
use crate::query_call_consume;
|
||||||
use crate::CallDB;
|
use crate::CallDB;
|
||||||
|
|
||||||
@ -42,6 +43,15 @@ impl MessageForward {
|
|||||||
Ok(self)
|
Ok(self)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub async fn store_db(self, db: &mut DB) -> DbResult<Self> {
|
||||||
|
let db = db.get_database().await;
|
||||||
|
let ci = db.collection::<Self>("message_forward");
|
||||||
|
|
||||||
|
ci.insert_one(&self).await?;
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get<D: CallDB>(
|
pub async fn get<D: CallDB>(
|
||||||
db: &mut D,
|
db: &mut D,
|
||||||
chat_id: i64,
|
chat_id: i64,
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
pub mod application;
|
pub mod application;
|
||||||
|
pub mod bots;
|
||||||
pub mod callback_info;
|
pub mod callback_info;
|
||||||
pub mod message_forward;
|
pub mod message_forward;
|
||||||
|
pub mod raw_calls;
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -11,7 +13,7 @@ use futures::stream::TryStreamExt;
|
|||||||
|
|
||||||
use mongodb::options::IndexOptions;
|
use mongodb::options::IndexOptions;
|
||||||
use mongodb::{bson::doc, options::ClientOptions, Client};
|
use mongodb::{bson::doc, options::ClientOptions, Client};
|
||||||
use mongodb::{Database, IndexModel};
|
use mongodb::{Collection, Database, IndexModel};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(EnumStringify)]
|
#[derive(EnumStringify)]
|
||||||
@ -140,14 +142,15 @@ pub struct Media {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DB {
|
pub struct DB {
|
||||||
client: Client,
|
client: Client,
|
||||||
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DB {
|
impl DB {
|
||||||
pub async fn new<S: Into<String>>(db_url: S) -> DbResult<Self> {
|
pub async fn new<S: Into<String>>(db_url: S, name: String) -> DbResult<Self> {
|
||||||
let options = ClientOptions::parse(db_url.into()).await?;
|
let options = ClientOptions::parse(db_url.into()).await?;
|
||||||
let client = Client::with_options(options)?;
|
let client = Client::with_options(options)?;
|
||||||
|
|
||||||
Ok(DB { client })
|
Ok(DB { client, name })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn migrate(&mut self) -> DbResult<()> {
|
pub async fn migrate(&mut self) -> DbResult<()> {
|
||||||
@ -184,18 +187,38 @@ impl DB {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn init<S: Into<String>>(db_url: S) -> DbResult<Self> {
|
pub async fn init<S: Into<String>>(db_url: S, name: String) -> DbResult<Self> {
|
||||||
let mut db = Self::new(db_url).await?;
|
let mut db = Self::new(db_url, name).await?;
|
||||||
db.migrate().await?;
|
db.migrate().await?;
|
||||||
|
|
||||||
Ok(db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_name(self, name: String) -> Self {
|
||||||
|
Self { name, ..self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DbCollection {
|
||||||
|
const COLLECTION: &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait GetCollection {
|
||||||
|
async fn get_collection<C: DbCollection + Send + Sync>(&mut self) -> Collection<C>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl CallDB for DB {
|
impl CallDB for DB {
|
||||||
async fn get_database(&mut self) -> Database {
|
async fn get_database(&mut self) -> Database {
|
||||||
self.client.database("gongbot")
|
self.client.database(&self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: CallDB> GetCollection for T {
|
||||||
|
async fn get_collection<C: DbCollection + Send + Sync>(&mut self) -> Collection<C> {
|
||||||
|
self.get_database()
|
||||||
|
.await
|
||||||
|
.collection(<C as DbCollection>::COLLECTION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
src/db/raw_calls.rs
Normal file
38
src/db/raw_calls.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
use mongodb::Database;
|
||||||
|
|
||||||
|
use super::CallDB;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum RawCallError {
|
||||||
|
#[error("error while processing mongodb query: {0}")]
|
||||||
|
MongodbError(#[from] mongodb::error::Error),
|
||||||
|
#[error("error while buildint bson's query document: {0}")]
|
||||||
|
DocumentError(#[from] mongodb::bson::extjson::de::Error),
|
||||||
|
#[error("error when expected map: {0}")]
|
||||||
|
NotAMapError(String),
|
||||||
|
}
|
||||||
|
pub type RawCallResult<T> = Result<T, RawCallError>;
|
||||||
|
|
||||||
|
pub trait RawCall {
|
||||||
|
async fn get_database(&mut self) -> Database;
|
||||||
|
async fn find_one(&mut self, collection: &str, query: Value) -> RawCallResult<Option<Value>> {
|
||||||
|
let db = self.get_database().await;
|
||||||
|
let value = db.collection::<Value>(collection);
|
||||||
|
|
||||||
|
let map = match query {
|
||||||
|
Value::Object(map) => map,
|
||||||
|
_ => return Err(RawCallError::NotAMapError("query is not a map".to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let doc = map.try_into()?;
|
||||||
|
let ret = value.find_one(doc).await?;
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: CallDB> RawCall for T {
|
||||||
|
async fn get_database(&mut self) -> Database {
|
||||||
|
CallDB::get_database(self).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ async fn setup_db() -> DB {
|
|||||||
dotenvy::dotenv().unwrap();
|
dotenvy::dotenv().unwrap();
|
||||||
let db_url = std::env::var("DATABASE_URL").unwrap();
|
let db_url = std::env::var("DATABASE_URL").unwrap();
|
||||||
|
|
||||||
DB::new(db_url).await.unwrap()
|
DB::new(db_url, "gongbot".to_string()).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
549
src/handlers/admin.rs
Normal file
549
src/handlers/admin.rs
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
use log::{info, warn};
|
||||||
|
use std::time::Duration;
|
||||||
|
use teloxide::dispatching::dialogue::serializer::Json;
|
||||||
|
use teloxide::net::Download;
|
||||||
|
use teloxide::prelude::*;
|
||||||
|
use teloxide::sugar::request::RequestReplyExt;
|
||||||
|
use teloxide::types::{MediaKind, MessageId, MessageKind, ParseMode};
|
||||||
|
use teloxide::utils::render::RenderMessageTextHelper;
|
||||||
|
use teloxide::{dptree, types::Update};
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
|
||||||
|
use crate::admin::{admin_command_handler, AdminCommands};
|
||||||
|
use crate::bot_handler::BotHandler;
|
||||||
|
use crate::db::bots::BotInstance;
|
||||||
|
use crate::db::message_forward::MessageForward;
|
||||||
|
use crate::db::{CallDB, DB};
|
||||||
|
use crate::mongodb_storage::MongodbStorage;
|
||||||
|
use crate::{BotDialogue, BotError, BotResult, CallbackStore, State};
|
||||||
|
|
||||||
|
pub fn admin_handler() -> BotHandler {
|
||||||
|
dptree::entry()
|
||||||
|
// keep on top to cancel any action
|
||||||
|
.branch(cancel_handler())
|
||||||
|
.branch(
|
||||||
|
Update::filter_callback_query()
|
||||||
|
.filter_async(async |q: CallbackQuery, mut db: DB| {
|
||||||
|
let tguser = q.from.clone();
|
||||||
|
let user = db
|
||||||
|
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
||||||
|
.await;
|
||||||
|
user.map(|u| u.is_admin).unwrap_or(false)
|
||||||
|
})
|
||||||
|
.enter_dialogue::<CallbackQuery, MongodbStorage<Json>, State>()
|
||||||
|
.branch(dptree::case![State::EditButton].endpoint(button_edit_callback)),
|
||||||
|
)
|
||||||
|
.branch(command_handler())
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter_async(async |msg: Message, mut db: DB| {
|
||||||
|
let tguser = match msg.from.clone() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return false, // do nothing, cause its not usecase of function
|
||||||
|
};
|
||||||
|
let user = db
|
||||||
|
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
||||||
|
.await;
|
||||||
|
user.map(|u| u.is_admin).unwrap_or(false)
|
||||||
|
})
|
||||||
|
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter(|msg: Message| {
|
||||||
|
msg.text().unwrap_or("").to_lowercase().as_str() == "edit"
|
||||||
|
})
|
||||||
|
.endpoint(edit_msg_cmd_handler),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter_map(|msg: Message| {
|
||||||
|
let text = msg.caption().unwrap_or("");
|
||||||
|
let mut parts = text.split_whitespace();
|
||||||
|
let cmd = parts.next().unwrap_or("");
|
||||||
|
let arg = parts.next().unwrap_or("");
|
||||||
|
|
||||||
|
match cmd.to_lowercase().as_str() == "/newscript" {
|
||||||
|
true => Some(arg.to_string()),
|
||||||
|
false => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.endpoint(newscript_handler),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.filter(|msg: Message| msg.reply_to_message().is_some())
|
||||||
|
.filter(|state: State| matches!(state, State::Start))
|
||||||
|
.endpoint(support_reply_handler),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
dptree::case![State::Edit {
|
||||||
|
literal,
|
||||||
|
variant,
|
||||||
|
lang,
|
||||||
|
is_caption_set
|
||||||
|
}]
|
||||||
|
.endpoint(edit_msg_handler),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.branch(
|
||||||
|
Update::filter_message()
|
||||||
|
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
||||||
|
.branch(dptree::case![State::MessageForwardReply].endpoint(user_reply_to_support)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async fn newscript_handler(bot: Bot, mut db: DB, msg: Message, name: String) -> BotResult<()> {
|
||||||
|
let script = match msg.kind {
|
||||||
|
MessageKind::Common(message) => {
|
||||||
|
match message.media_kind {
|
||||||
|
MediaKind::Document(media_document) => {
|
||||||
|
let doc = media_document.document;
|
||||||
|
let file = bot.get_file(doc.file.id).await?;
|
||||||
|
let mut stream = bot.download_file_stream(&file.path);
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
while let Some(bytes) = stream.next().await {
|
||||||
|
let mut bytes = bytes.unwrap().to_vec();
|
||||||
|
buf.append(&mut bytes);
|
||||||
|
}
|
||||||
|
let script = match String::from_utf8(buf) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to parse buf to string, err: {err}");
|
||||||
|
bot.send_message(msg.chat.id, format!("Failed to Convert file to script: file is not UTF-8, err: {err}")).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
script
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match BotInstance::get_by_name(&mut db, &name).await? {
|
||||||
|
Some(bi) => bi,
|
||||||
|
None => {
|
||||||
|
bot.send_message(
|
||||||
|
msg.chat.id,
|
||||||
|
format!("Failed to set script, possibly bots name is incorrent"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
BotInstance::update_script(&mut db, &name, &script).await?;
|
||||||
|
|
||||||
|
bot.send_message(msg.chat.id, "New script is set!").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn button_edit_callback(
|
||||||
|
bot: Bot,
|
||||||
|
mut db: DB,
|
||||||
|
dialogue: BotDialogue,
|
||||||
|
q: CallbackQuery,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
bot.answer_callback_query(&q.id).await?;
|
||||||
|
|
||||||
|
let id = match q.data {
|
||||||
|
Some(id) => id,
|
||||||
|
None => {
|
||||||
|
bot.send_message(q.from.id, "Not compatible callback to edit text on")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ci = match CallbackStore::get(&mut db, &id).await? {
|
||||||
|
Some(ci) => ci,
|
||||||
|
None => {
|
||||||
|
bot.send_message(
|
||||||
|
q.from.id,
|
||||||
|
"Can't get button information. Maybe created not by this bot or message too old",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let literal = match ci.literal {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
bot.send_message(
|
||||||
|
q.from.id,
|
||||||
|
"This button is not editable (probably text is generated)",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let lang = "ru".to_string();
|
||||||
|
dialogue
|
||||||
|
.update(State::Edit {
|
||||||
|
literal,
|
||||||
|
variant: None,
|
||||||
|
lang,
|
||||||
|
is_caption_set: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
bot.send_message(q.from.id, "Send text of button").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_handler() -> BotHandler {
|
||||||
|
Update::filter_message()
|
||||||
|
.filter(|msg: Message| msg.text() == Some("/cancel"))
|
||||||
|
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
||||||
|
.endpoint(async |bot: Bot, msg: Message, dialogue: BotDialogue| {
|
||||||
|
dialogue.exit().await?;
|
||||||
|
bot.send_message(msg.chat.id, "Диалог закончен!").await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_handler() -> BotHandler {
|
||||||
|
Update::filter_message()
|
||||||
|
.filter_async(async |msg: Message, mut db: DB| {
|
||||||
|
let tguser = match msg.from.clone() {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return false, // do nothing, cause its not usecase of function
|
||||||
|
};
|
||||||
|
let user = db
|
||||||
|
.get_or_init_user(tguser.id.0 as i64, &tguser.first_name)
|
||||||
|
.await;
|
||||||
|
user.map(|u| u.is_admin).unwrap_or(false)
|
||||||
|
})
|
||||||
|
.filter_command::<AdminCommands>()
|
||||||
|
.enter_dialogue::<Message, MongodbStorage<Json>, State>()
|
||||||
|
.endpoint(admin_command_handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn edit_msg_cmd_handler(
|
||||||
|
bot: Bot,
|
||||||
|
mut db: DB,
|
||||||
|
dialogue: BotDialogue,
|
||||||
|
msg: Message,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
match msg.reply_to_message() {
|
||||||
|
Some(replied) => {
|
||||||
|
let msgid = replied.id;
|
||||||
|
// look for message in db and set text
|
||||||
|
let literal = match db.get_message_literal(msg.chat.id.0, msgid.0).await? {
|
||||||
|
Some(l) => l,
|
||||||
|
None => {
|
||||||
|
bot.send_message(msg.chat.id, "No such message found to edit. Look if you replying bot's message and this message is supposed to be editable").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// TODO: language selector will be implemented in future 😈
|
||||||
|
let lang = "ru".to_string();
|
||||||
|
dialogue
|
||||||
|
.update(State::Edit {
|
||||||
|
literal,
|
||||||
|
variant: None,
|
||||||
|
lang,
|
||||||
|
is_caption_set: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
bot.send_message(
|
||||||
|
msg.chat.id,
|
||||||
|
"Ok, now you have to send message text (formatting supported)\n\
|
||||||
|
<b>Notice:</b> if this message supposed to replace message (tg shows them as edited) \
|
||||||
|
or be raplaced, do NOT send message with multiple media, only single photo, video etc. \
|
||||||
|
To get more information about why, see in /why_media_group",
|
||||||
|
).parse_mode(ParseMode::Html)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
bot.send_message(msg.chat.id, "You have to reply to message to edit it")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn support_reply_handler(
|
||||||
|
bot: Bot,
|
||||||
|
mut db: DB,
|
||||||
|
msg: Message,
|
||||||
|
state_mgr: std::sync::Arc<MongodbStorage<Json>>,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
use teloxide::utils::render::Renderer;
|
||||||
|
|
||||||
|
let rm = match msg.reply_to_message() {
|
||||||
|
Some(rm) => rm,
|
||||||
|
None => {
|
||||||
|
return Err(BotError::BotLogicError(
|
||||||
|
"support_reply_handler should not be called when no message is replied".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (chat_id, message_id) = (rm.chat.id.0, rm.id.0);
|
||||||
|
let mf = match MessageForward::get(&mut db, chat_id, message_id).await? {
|
||||||
|
Some(mf) => mf,
|
||||||
|
None => {
|
||||||
|
bot.send_message(msg.chat.id, "No forwarded message found for your reply")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = match msg.kind {
|
||||||
|
MessageKind::Common(message_common) => match message_common.media_kind {
|
||||||
|
MediaKind::Text(media_text) => {
|
||||||
|
Renderer::new(&media_text.text, &media_text.entities).as_html()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
bot.send_message(msg.chat.id, "Only text messages currently supported!")
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// can't hapen because we already have check for reply
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = format!(
|
||||||
|
"Сообщение от поддержки:\n{}\nЧтобы закончить диалог, нажмите на /cancel",
|
||||||
|
text
|
||||||
|
);
|
||||||
|
let msg = bot
|
||||||
|
.send_message(ChatId(mf.source_chat_id), text)
|
||||||
|
.parse_mode(ParseMode::Html);
|
||||||
|
let msg = match mf.reply {
|
||||||
|
false => msg,
|
||||||
|
true => msg.reply_to(MessageId(mf.source_message_id)),
|
||||||
|
};
|
||||||
|
msg.await?;
|
||||||
|
|
||||||
|
let user_dialogue = BotDialogue::new(state_mgr, ChatId(mf.source_chat_id));
|
||||||
|
user_dialogue.update(State::MessageForwardReply).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn edit_msg_handler(
|
||||||
|
bot: Bot,
|
||||||
|
mut db: DB,
|
||||||
|
dialogue: BotDialogue,
|
||||||
|
(literal, variant, lang, is_caption_set): (String, Option<String>, String, bool),
|
||||||
|
msg: Message,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
use teloxide::utils::render::Renderer;
|
||||||
|
|
||||||
|
let chat_id = msg.chat.id;
|
||||||
|
info!("Type: {:#?}", msg.kind);
|
||||||
|
let msg = if let MessageKind::Common(msg) = msg.kind {
|
||||||
|
msg
|
||||||
|
} else {
|
||||||
|
info!("Not a Common, somehow");
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(variant) = variant {
|
||||||
|
if let MediaKind::Text(text) = msg.media_kind {
|
||||||
|
let html_text = Renderer::new(&text.text, &text.entities).as_html();
|
||||||
|
|
||||||
|
db.set_literal_alternative(&literal, &variant, &html_text)
|
||||||
|
.await?;
|
||||||
|
bot.send_message(chat_id, "Updated text of variant!")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
dialogue.exit().await?;
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
bot.send_message(
|
||||||
|
chat_id,
|
||||||
|
"On variants only text alternating supported. Try to send text only",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match msg.media_kind {
|
||||||
|
MediaKind::Text(text) => {
|
||||||
|
db.drop_media(&literal).await?;
|
||||||
|
if is_caption_set {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let html_text = Renderer::new(&text.text, &text.entities).as_html();
|
||||||
|
db.set_literal(&literal, &html_text).await?;
|
||||||
|
bot.send_message(chat_id, "Updated text of message!")
|
||||||
|
.await?;
|
||||||
|
dialogue.exit().await?;
|
||||||
|
}
|
||||||
|
MediaKind::Photo(photo) => {
|
||||||
|
let group = photo.media_group_id;
|
||||||
|
if let Some(group) = group.clone() {
|
||||||
|
db.drop_media_except(&literal, &group).await?;
|
||||||
|
} else {
|
||||||
|
db.drop_media(&literal).await?;
|
||||||
|
}
|
||||||
|
let file_id = photo.photo[0].file.id.clone();
|
||||||
|
db.add_media(&literal, "photo", &file_id, group.as_deref())
|
||||||
|
.await?;
|
||||||
|
match photo.caption {
|
||||||
|
Some(text) => {
|
||||||
|
let html_text = Renderer::new(&text, &photo.caption_entities).as_html();
|
||||||
|
db.set_literal(&literal, &html_text).await?;
|
||||||
|
bot.send_message(chat_id, "Updated photo caption!").await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// if it is a first message in group,
|
||||||
|
// or just a photo without caption (unwrap_or case),
|
||||||
|
// set text empty
|
||||||
|
if !db
|
||||||
|
.is_media_group_exists(group.as_deref().unwrap_or(""))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
db.set_literal(&literal, "").await?;
|
||||||
|
bot.send_message(chat_id, "Set photo without caption")
|
||||||
|
.await?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Some workaround because Telegram's group system
|
||||||
|
// is not easily and obviously handled with this
|
||||||
|
// code architecture, but probably there is a solution.
|
||||||
|
//
|
||||||
|
// So, this code will just wait for all media group
|
||||||
|
// updates to be processed
|
||||||
|
dialogue
|
||||||
|
.update(State::Edit {
|
||||||
|
literal,
|
||||||
|
variant: None,
|
||||||
|
lang,
|
||||||
|
is_caption_set: true,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
dialogue.exit().await.unwrap_or(());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
MediaKind::Video(video) => {
|
||||||
|
let group = video.media_group_id;
|
||||||
|
if let Some(group) = group.clone() {
|
||||||
|
db.drop_media_except(&literal, &group).await?;
|
||||||
|
} else {
|
||||||
|
db.drop_media(&literal).await?;
|
||||||
|
}
|
||||||
|
let file_id = video.video.file.id;
|
||||||
|
db.add_media(&literal, "video", &file_id, group.as_deref())
|
||||||
|
.await?;
|
||||||
|
match video.caption {
|
||||||
|
Some(text) => {
|
||||||
|
let html_text = Renderer::new(&text, &video.caption_entities).as_html();
|
||||||
|
db.set_literal(&literal, &html_text).await?;
|
||||||
|
bot.send_message(chat_id, "Updated video caption!").await?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// if it is a first message in group,
|
||||||
|
// or just a video without caption (unwrap_or case),
|
||||||
|
// set text empty
|
||||||
|
if !db
|
||||||
|
.is_media_group_exists(group.as_deref().unwrap_or(""))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
db.set_literal(&literal, "").await?;
|
||||||
|
bot.send_message(chat_id, "Set video without caption")
|
||||||
|
.await?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Some workaround because Telegram's group system
|
||||||
|
// is not easily and obviously handled with this
|
||||||
|
// code architecture, but probably there is a solution.
|
||||||
|
//
|
||||||
|
// So, this code will just wait for all media group
|
||||||
|
// updates to be processed
|
||||||
|
dialogue
|
||||||
|
.update(State::Edit {
|
||||||
|
literal,
|
||||||
|
variant: None,
|
||||||
|
lang,
|
||||||
|
is_caption_set: true,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
dialogue.exit().await.unwrap_or(());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
bot.send_message(chat_id, "this type of message is not supported yet")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn user_reply_to_support(bot: Bot, mut db: DB, msg: Message) -> BotResult<()> {
|
||||||
|
let (source_chat_id, source_message_id) = (msg.chat.id.0, msg.id.0);
|
||||||
|
let text = match msg.html_text() {
|
||||||
|
Some(text) => text,
|
||||||
|
// TODO: come up with better idea than just ignoring (say something to user)
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
let scid =
|
||||||
|
db.get_literal_value("support_chat_id")
|
||||||
|
.await?
|
||||||
|
.ok_or(BotError::AdminMisconfiguration(
|
||||||
|
"support_chat_id is not set".to_string(),
|
||||||
|
))?;
|
||||||
|
let support_chat_id = match scid.parse::<i64>() {
|
||||||
|
Ok(cid) => cid,
|
||||||
|
Err(parseerr) => {
|
||||||
|
return Err(BotError::BotLogicError(format!(
|
||||||
|
"source_chat_id, got: {scid}, expected: i64, err: {parseerr}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let user = msg.from.ok_or(BotError::BotLogicError(
|
||||||
|
"Unable to get user somehow:/".to_string(),
|
||||||
|
))?;
|
||||||
|
let parts = [
|
||||||
|
Some(user.first_name),
|
||||||
|
user.last_name,
|
||||||
|
user.username.map(|un| format!("(@{un})")),
|
||||||
|
];
|
||||||
|
#[allow(unstable_name_collisions)]
|
||||||
|
let userformat: String = parts
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.intersperse(" ".to_string())
|
||||||
|
.collect();
|
||||||
|
let msgtext = format!("From: {userformat}\nMessage:\n{text}");
|
||||||
|
|
||||||
|
// TODO: fix bug: parse mode's purpose is to display user-formated text in right way,
|
||||||
|
// but there is a bug: user can inject html code with his first/last/user name
|
||||||
|
// it's not harmful, only visible to support, but still need a fix
|
||||||
|
let sentmsg = bot
|
||||||
|
.send_message(ChatId(support_chat_id), msgtext)
|
||||||
|
.parse_mode(ParseMode::Html)
|
||||||
|
.await?;
|
||||||
|
MessageForward::new(
|
||||||
|
sentmsg.chat.id.0,
|
||||||
|
sentmsg.id.0,
|
||||||
|
source_chat_id,
|
||||||
|
source_message_id,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.store(&mut db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
1
src/handlers/mod.rs
Normal file
1
src/handlers/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod admin;
|
||||||
986
src/main.rs
986
src/main.rs
File diff suppressed because it is too large
Load Diff
302
src/message_answerer.rs
Normal file
302
src/message_answerer.rs
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
use log::{info, warn};
|
||||||
|
use teloxide::prelude::*;
|
||||||
|
use teloxide::types::{
|
||||||
|
InputFile, InputMedia, InputMediaPhoto, InputMediaVideo, MessageId, ParseMode,
|
||||||
|
};
|
||||||
|
use teloxide::{
|
||||||
|
types::{ChatId, InlineKeyboardMarkup},
|
||||||
|
Bot,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::db::Media;
|
||||||
|
use crate::{
|
||||||
|
db::{CallDB, DB},
|
||||||
|
notify_admin, BotResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! send_media {
|
||||||
|
($self:ident, $method:ident, $chat_id:expr, $file_id: expr, $text: expr, $keyboard: expr) => {{
|
||||||
|
let msg = $self
|
||||||
|
.bot
|
||||||
|
.$method(ChatId($chat_id), InputFile::file_id($file_id.to_string()));
|
||||||
|
let msg = match $text.as_str() {
|
||||||
|
"" => msg,
|
||||||
|
text => msg.caption(text),
|
||||||
|
};
|
||||||
|
let msg = match $keyboard {
|
||||||
|
Some(kbd) => msg.reply_markup(kbd),
|
||||||
|
None => msg,
|
||||||
|
};
|
||||||
|
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
|
||||||
|
|
||||||
|
let msg = msg.await?;
|
||||||
|
Ok((msg.chat.id.0, msg.id.0))
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageAnswerer<'a> {
|
||||||
|
bot: &'a Bot,
|
||||||
|
chat_id: i64,
|
||||||
|
db: &'a mut DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MessageAnswerer<'a> {
|
||||||
|
pub fn new(bot: &'a Bot, db: &'a mut DB, chat_id: i64) -> Self {
|
||||||
|
Self { bot, chat_id, db }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_text(
|
||||||
|
&mut self,
|
||||||
|
literal: &str,
|
||||||
|
variant: Option<&str>,
|
||||||
|
is_replace: bool,
|
||||||
|
) -> BotResult<String> {
|
||||||
|
let variant_text = match variant {
|
||||||
|
Some(variant) => {
|
||||||
|
let value = self
|
||||||
|
.db
|
||||||
|
.get_literal_alternative_value(literal, variant)
|
||||||
|
.await?;
|
||||||
|
if value.is_none() && !is_replace {
|
||||||
|
notify_admin(&format!("variant {variant} for literal {literal} is not found! falling back to just literal")).await;
|
||||||
|
}
|
||||||
|
value
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let text = match variant_text {
|
||||||
|
Some(text) => text,
|
||||||
|
None => self
|
||||||
|
.db
|
||||||
|
.get_literal_value(literal)
|
||||||
|
.await?
|
||||||
|
.unwrap_or("Please, set content of this message".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn answer(
|
||||||
|
mut self,
|
||||||
|
literal: &str,
|
||||||
|
variant: Option<&str>,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult<(i64, i32)> {
|
||||||
|
let text = self.get_text(literal, variant, false).await?;
|
||||||
|
self.answer_inner(text, literal, variant, keyboard).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn answer_inner(
|
||||||
|
mut self,
|
||||||
|
text: String,
|
||||||
|
literal: &str,
|
||||||
|
variant: Option<&str>,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult<(i64, i32)> {
|
||||||
|
let media = self.db.get_media(literal).await?;
|
||||||
|
let (chat_id, msg_id) = match media.len() {
|
||||||
|
// just a text
|
||||||
|
0 => self.send_message(text, keyboard).await?,
|
||||||
|
// single media
|
||||||
|
1 => self.send_media(&media[0], text, keyboard).await?,
|
||||||
|
// >= 2, should use media group
|
||||||
|
_ => self.send_media_group(media, text).await?,
|
||||||
|
};
|
||||||
|
self.store_message_info(msg_id, literal, variant).await?;
|
||||||
|
Ok((chat_id, msg_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn replace_message(
|
||||||
|
mut self,
|
||||||
|
message_id: i32,
|
||||||
|
literal: &str,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
let variant = self
|
||||||
|
.db
|
||||||
|
.get_message(self.chat_id, message_id)
|
||||||
|
.await?
|
||||||
|
.and_then(|m| m.variant);
|
||||||
|
let text = self.get_text(literal, variant.as_deref(), true).await?;
|
||||||
|
let media = self.db.get_media(literal).await?;
|
||||||
|
let (chat_id, msg_id) = match media.len() {
|
||||||
|
// just a text
|
||||||
|
0 => {
|
||||||
|
let msg =
|
||||||
|
self.bot
|
||||||
|
.edit_message_text(ChatId(self.chat_id), MessageId(message_id), &text);
|
||||||
|
let msg = match keyboard {
|
||||||
|
Some(ref kbd) => msg.reply_markup(kbd.clone()),
|
||||||
|
None => msg,
|
||||||
|
};
|
||||||
|
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
|
||||||
|
info!("ENTS: {:?}", msg.entities);
|
||||||
|
let msg = match msg.await {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(teloxide::RequestError::Api(teloxide::ApiError::Unknown(errtext)))
|
||||||
|
if errtext.as_str()
|
||||||
|
== "Bad Request: there is no text in the message to edit" =>
|
||||||
|
{
|
||||||
|
// fallback to sending message
|
||||||
|
warn!("Fallback into sending message instead of editing because it contains media");
|
||||||
|
self.answer_inner(text, literal, variant.as_deref(), keyboard)
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
(msg.chat.id.0, msg.id.0)
|
||||||
|
}
|
||||||
|
// single media
|
||||||
|
1 => {
|
||||||
|
let media = &media[0]; // safe, cause we just checked len
|
||||||
|
let input_file = InputFile::file_id(media.file_id.to_string());
|
||||||
|
let media = match media.media_type.as_str() {
|
||||||
|
"photo" => InputMedia::Photo(teloxide::types::InputMediaPhoto::new(input_file)),
|
||||||
|
"video" => InputMedia::Video(teloxide::types::InputMediaVideo::new(input_file)),
|
||||||
|
_ => todo!(),
|
||||||
|
};
|
||||||
|
self.bot
|
||||||
|
.edit_message_media(ChatId(self.chat_id), MessageId(message_id), media)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let msg = self
|
||||||
|
.bot
|
||||||
|
.edit_message_caption(ChatId(self.chat_id), MessageId(message_id));
|
||||||
|
let msg = match text.as_str() {
|
||||||
|
"" => msg,
|
||||||
|
text => msg.caption(text),
|
||||||
|
};
|
||||||
|
let msg = match keyboard {
|
||||||
|
Some(kbd) => msg.reply_markup(kbd),
|
||||||
|
None => msg,
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
|
||||||
|
let msg = msg.await?;
|
||||||
|
|
||||||
|
(msg.chat.id.0, msg.id.0)
|
||||||
|
}
|
||||||
|
// >= 2, should use media group
|
||||||
|
_ => {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.store_message_info(msg_id, literal, variant.as_deref())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn store_message_info(
|
||||||
|
&mut self,
|
||||||
|
message_id: i32,
|
||||||
|
literal: &str,
|
||||||
|
variant: Option<&str>,
|
||||||
|
) -> BotResult<()> {
|
||||||
|
match variant {
|
||||||
|
Some(variant) => {
|
||||||
|
self.db
|
||||||
|
.set_message_literal_variant(self.chat_id, message_id, literal, variant)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.db
|
||||||
|
.set_message_literal(self.chat_id, message_id, literal)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_message(
|
||||||
|
&self,
|
||||||
|
text: String,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult<(i64, i32)> {
|
||||||
|
let msg = self.bot.send_message(ChatId(self.chat_id), text);
|
||||||
|
let msg = match keyboard {
|
||||||
|
Some(kbd) => msg.reply_markup(kbd),
|
||||||
|
None => msg,
|
||||||
|
};
|
||||||
|
let msg = msg.parse_mode(teloxide::types::ParseMode::Html);
|
||||||
|
info!("ENTS: {:?}", msg.entities);
|
||||||
|
let msg = msg.await?;
|
||||||
|
|
||||||
|
Ok((msg.chat.id.0, msg.id.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_media(
|
||||||
|
&self,
|
||||||
|
media: &Media,
|
||||||
|
text: String,
|
||||||
|
keyboard: Option<InlineKeyboardMarkup>,
|
||||||
|
) -> BotResult<(i64, i32)> {
|
||||||
|
match media.media_type.as_str() {
|
||||||
|
"photo" => {
|
||||||
|
send_media!(
|
||||||
|
self,
|
||||||
|
send_photo,
|
||||||
|
self.chat_id,
|
||||||
|
media.file_id,
|
||||||
|
text,
|
||||||
|
keyboard
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"video" => {
|
||||||
|
send_media!(
|
||||||
|
self,
|
||||||
|
send_video,
|
||||||
|
self.chat_id,
|
||||||
|
media.file_id,
|
||||||
|
text,
|
||||||
|
keyboard
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_media_group(&self, media: Vec<Media>, text: String) -> BotResult<(i64, i32)> {
|
||||||
|
let media: Vec<InputMedia> = media
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, m)| {
|
||||||
|
let ifile = InputFile::file_id(m.file_id);
|
||||||
|
let caption = if i == 0 {
|
||||||
|
match text.as_str() {
|
||||||
|
"" => None,
|
||||||
|
text => Some(text.to_string()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
match m.media_type.as_str() {
|
||||||
|
"photo" => InputMedia::Photo(InputMediaPhoto {
|
||||||
|
caption,
|
||||||
|
parse_mode: Some(ParseMode::Html),
|
||||||
|
..InputMediaPhoto::new(ifile)
|
||||||
|
}),
|
||||||
|
"video" => InputMedia::Video(InputMediaVideo {
|
||||||
|
caption,
|
||||||
|
parse_mode: Some(ParseMode::Html),
|
||||||
|
..InputMediaVideo::new(ifile)
|
||||||
|
}),
|
||||||
|
_ => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let msg = self.bot.send_media_group(ChatId(self.chat_id), media);
|
||||||
|
|
||||||
|
let msg = msg.await?;
|
||||||
|
|
||||||
|
Ok((msg[0].chat.id.0, msg[0].id.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,8 @@ use mongodb::Database;
|
|||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use teloxide::dispatching::dialogue::{Serializer, Storage};
|
use teloxide::dispatching::dialogue::{Serializer, Storage};
|
||||||
|
|
||||||
|
use crate::db::{CallDB, DB};
|
||||||
|
|
||||||
pub struct MongodbStorage<S> {
|
pub struct MongodbStorage<S> {
|
||||||
database: Database,
|
database: Database,
|
||||||
serializer: S,
|
serializer: S,
|
||||||
@ -28,6 +30,13 @@ impl<S> MongodbStorage<S> {
|
|||||||
serializer,
|
serializer,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn from_db(db: &mut DB, serializer: S) -> Result<Arc<Self>, mongodb::error::Error> {
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
database: CallDB::get_database(db).await,
|
||||||
|
serializer,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
pub mod parcelable;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use teloxide::types::InlineKeyboardButton;
|
use teloxide::types::InlineKeyboardButton;
|
||||||
|
|
||||||
|
|||||||
110
src/utils/parcelable.rs
Normal file
110
src/utils/parcelable.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub enum ParcelType<'a, F> {
|
||||||
|
Function(&'a mut F),
|
||||||
|
Parcelable(&'a mut dyn Parcelable<F>),
|
||||||
|
Other(()),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ParcelableError {
|
||||||
|
#[error("error to get field: {0:?}")]
|
||||||
|
FieldError(String),
|
||||||
|
#[error("error when addressing nested element: {0:?}")]
|
||||||
|
NestError(String),
|
||||||
|
#[error("error to resolve Parcelable: {0:?}")]
|
||||||
|
ResolveError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ParcelableResult<T> = Result<T, ParcelableError>;
|
||||||
|
|
||||||
|
pub trait Parcelable<F> {
|
||||||
|
fn get_field(&mut self, name: &str) -> ParcelableResult<ParcelType<F>>;
|
||||||
|
|
||||||
|
fn resolve(&mut self) -> ParcelableResult<ParcelType<F>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
let root = ParcelableResult::Ok(ParcelType::Parcelable(self));
|
||||||
|
root
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get nested field by name, which is fields joined by dot
|
||||||
|
/// for example: passing name "field1.somefield" will be the same
|
||||||
|
/// as using `struct.field1.somefield`, by dynamically
|
||||||
|
fn get_nested(&mut self, name: &str) -> ParcelableResult<ParcelType<F>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
let root = ParcelableResult::Ok(ParcelType::Parcelable(self));
|
||||||
|
name.split('.')
|
||||||
|
.fold(root, |s: ParcelableResult<ParcelType<F>>, field| match s? {
|
||||||
|
ParcelType::Parcelable(p) => p.get_field(field),
|
||||||
|
_ => Err(ParcelableError::NestError(format!(
|
||||||
|
"Failed to get field {field}. End of nestment"
|
||||||
|
))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F> Parcelable<F> for String {
|
||||||
|
fn get_field(&mut self, _name: &str) -> ParcelableResult<ParcelType<F>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&mut self) -> ParcelableResult<ParcelType<F>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
Ok(ParcelType::Other(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, T: Parcelable<F>> Parcelable<F> for Option<T> {
|
||||||
|
fn get_field(&mut self, name: &str) -> ParcelableResult<ParcelType<F>> {
|
||||||
|
Err(ParcelableError::FieldError(format!(
|
||||||
|
"tried to get field {name}, but calls of get_field are not allowed on Option"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&mut self) -> crate::utils::parcelable::ParcelableResult<ParcelType<F>>
|
||||||
|
where
|
||||||
|
Self: Sized + 'static,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Some(v) => Ok(v.resolve()?),
|
||||||
|
None => Err(ParcelableError::ResolveError("Option was None".to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, V: Parcelable<F> + 'static> Parcelable<F> for HashMap<String, V> {
|
||||||
|
fn get_field(&mut self, name: &str) -> ParcelableResult<ParcelType<F>> {
|
||||||
|
match self.get_mut(name) {
|
||||||
|
Some(v) => Ok(Parcelable::resolve(v)?),
|
||||||
|
None => Err(ParcelableError::FieldError(format!(
|
||||||
|
"tried to get value by key {name}, but this key does not exists"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, T: Parcelable<F> + 'static> Parcelable<F> for Vec<T> {
|
||||||
|
fn get_field(&mut self, name: &str) -> ParcelableResult<ParcelType<F>> {
|
||||||
|
let index: usize = match name.parse() {
|
||||||
|
Ok(index) => index,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(ParcelableError::FieldError(format!(
|
||||||
|
"Failed to parse field name `{name}` as an array index, err: {err}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let veclen = self.len();
|
||||||
|
let value = match self.get_mut(index) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Err(ParcelableError::FieldError(format!("Failed to get vec element with index {index}, probably out of bound (vec len: {veclen})"))),
|
||||||
|
};
|
||||||
|
|
||||||
|
Parcelable::resolve(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user