mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-10-23 16:59:08 +00:00
Added telegram
This commit is contained in:
302
Cargo.lock
generated
302
Cargo.lock
generated
@@ -126,6 +126,20 @@ version = "1.0.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
|
||||
[[package]]
|
||||
name = "aquamarine"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21cc1548309245035eb18aa7f0967da6bc65587005170c56e6ef2788a4cf3f4e"
|
||||
dependencies = [
|
||||
"include_dir",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arraydeque"
|
||||
version = "0.5.1"
|
||||
@@ -532,7 +546,7 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -594,7 +608,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"convert_case",
|
||||
"convert_case 0.6.0",
|
||||
"json5",
|
||||
"nom",
|
||||
"pathdiff",
|
||||
@@ -632,6 +646,12 @@ dependencies = [
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
@@ -733,14 +753,38 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
|
||||
dependencies = [
|
||||
"darling_core 0.13.4",
|
||||
"darling_macro 0.13.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.20.11",
|
||||
"darling_macro 0.20.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -753,17 +797,28 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
|
||||
dependencies = [
|
||||
"darling_core 0.13.4",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.20.11",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
@@ -789,6 +844,19 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"convert_case 0.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -827,6 +895,15 @@ version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "dptree"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d81175dab5ec79c30e0576df2ed2c244e1721720c302000bb321b107e82e265c"
|
||||
dependencies = [
|
||||
"futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
@@ -857,6 +934,16 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erasable"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "437cfb75878119ed8265685c41a115724eae43fb7cc5a0bf0e4ecc3b803af1c4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -864,7 +951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -974,6 +1061,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
@@ -1024,6 +1112,17 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
@@ -1042,8 +1141,10 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
@@ -1575,6 +1676,25 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -1655,6 +1775,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -2525,6 +2654,15 @@ dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rc-box"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0"
|
||||
dependencies = [
|
||||
"erasable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.12.1"
|
||||
@@ -2618,6 +2756,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
@@ -2632,10 +2771,12 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 0.25.4",
|
||||
"winreg",
|
||||
@@ -2754,6 +2895,15 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.2"
|
||||
@@ -2764,7 +2914,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3047,7 +3197,7 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.11",
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3120,6 +3270,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.225"
|
||||
@@ -3195,6 +3351,28 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_with_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
|
||||
dependencies = [
|
||||
"darling 0.13.4",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
@@ -3556,6 +3734,12 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -3640,12 +3824,92 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "take_mut"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60"
|
||||
|
||||
[[package]]
|
||||
name = "takecell"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e"
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "teloxide"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f79dd283eb21b90451c03fa7c7f83b9985130efb876b33bad89a2c208ccbc16"
|
||||
dependencies = [
|
||||
"aquamarine",
|
||||
"bytes",
|
||||
"derive_more",
|
||||
"dptree",
|
||||
"either",
|
||||
"futures",
|
||||
"log",
|
||||
"mime",
|
||||
"pin-project",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"teloxide-core",
|
||||
"teloxide-macros",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "teloxide-core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1642a7ef10e7af63b8298c8d13c0f986d4fc646d42649ff060359607f62f69"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"derive_more",
|
||||
"either",
|
||||
"futures",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"pin-project",
|
||||
"rc-box",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"take_mut",
|
||||
"takecell",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "teloxide-macros"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e2d33d809c3e7161a9ab18bedddf98821245014f0a78fa4d2c9430b2ec018c1"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.22.0"
|
||||
@@ -3656,7 +3920,7 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4261,7 +4525,7 @@ version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.11",
|
||||
"once_cell",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
@@ -4408,6 +4672,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.80"
|
||||
@@ -4467,7 +4744,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4898,6 +5175,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"teloxide",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
|
@@ -65,5 +65,8 @@ rustls = { version = "0.23", features = ["aws-lc-rs"] } # TLS library with aws-
|
||||
ring = "0.17" # Crypto for ACME
|
||||
pem = "3.0" # PEM format support
|
||||
|
||||
# Telegram bot support
|
||||
teloxide = { version = "0.13", features = ["macros"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
@@ -6,6 +6,7 @@ pub mod server;
|
||||
pub mod server_inbound;
|
||||
pub mod user_access;
|
||||
pub mod inbound_users;
|
||||
pub mod telegram_config;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::user::Entity as User;
|
||||
@@ -16,4 +17,5 @@ pub mod prelude {
|
||||
pub use super::server_inbound::Entity as ServerInbound;
|
||||
pub use super::user_access::Entity as UserAccess;
|
||||
pub use super::inbound_users::Entity as InboundUsers;
|
||||
pub use super::telegram_config::Entity as TelegramConfig;
|
||||
}
|
94
src/database/entities/telegram_config.rs
Normal file
94
src/database/entities/telegram_config.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "telegram_config")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: Uuid,
|
||||
|
||||
/// Telegram bot token (encrypted in production)
|
||||
pub bot_token: String,
|
||||
|
||||
/// Whether the bot is active
|
||||
pub is_active: bool,
|
||||
|
||||
/// When the config was created
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
/// Last time config was updated
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {
|
||||
/// Called before insert and update
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Called before update
|
||||
fn before_save<'life0, 'async_trait, C>(
|
||||
mut self,
|
||||
_db: &'life0 C,
|
||||
insert: bool,
|
||||
) -> core::pin::Pin<Box<dyn core::future::Future<Output = Result<Self, DbErr>> + Send + 'async_trait>>
|
||||
where
|
||||
'life0: 'async_trait,
|
||||
C: 'async_trait + ConnectionTrait,
|
||||
Self: 'async_trait,
|
||||
{
|
||||
Box::pin(async move {
|
||||
if !insert {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
} else if self.id.is_not_set() {
|
||||
self.id = Set(Uuid::new_v4());
|
||||
}
|
||||
|
||||
if self.created_at.is_not_set() {
|
||||
self.created_at = Set(chrono::Utc::now());
|
||||
}
|
||||
|
||||
if self.updated_at.is_not_set() {
|
||||
self.updated_at = Set(chrono::Utc::now());
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// DTO for creating a new Telegram configuration
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateTelegramConfigDto {
|
||||
pub bot_token: String,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
/// DTO for updating Telegram configuration
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateTelegramConfigDto {
|
||||
pub bot_token: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Convert to ActiveModel for updates
|
||||
pub fn into_active_model(self) -> ActiveModel {
|
||||
ActiveModel {
|
||||
id: Set(self.id),
|
||||
bot_token: Set(self.bot_token),
|
||||
is_active: Set(self.is_active),
|
||||
created_at: Set(self.created_at),
|
||||
updated_at: Set(self.updated_at),
|
||||
}
|
||||
}
|
||||
}
|
@@ -18,6 +18,9 @@ pub struct Model {
|
||||
/// Optional Telegram user ID for bot integration
|
||||
pub telegram_id: Option<i64>,
|
||||
|
||||
/// Whether the user is a Telegram admin
|
||||
pub is_telegram_admin: bool,
|
||||
|
||||
/// When the user was registered/created
|
||||
pub created_at: DateTimeUtc,
|
||||
|
||||
@@ -33,6 +36,7 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: Set(Uuid::new_v4()),
|
||||
is_telegram_admin: Set(false),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
..ActiveModelTrait::default()
|
||||
@@ -65,6 +69,8 @@ pub struct CreateUserDto {
|
||||
pub name: String,
|
||||
pub comment: Option<String>,
|
||||
pub telegram_id: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub is_telegram_admin: bool,
|
||||
}
|
||||
|
||||
/// User update data transfer object
|
||||
@@ -73,6 +79,7 @@ pub struct UpdateUserDto {
|
||||
pub name: Option<String>,
|
||||
pub comment: Option<String>,
|
||||
pub telegram_id: Option<i64>,
|
||||
pub is_telegram_admin: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<CreateUserDto> for ActiveModel {
|
||||
@@ -81,6 +88,7 @@ impl From<CreateUserDto> for ActiveModel {
|
||||
name: Set(dto.name),
|
||||
comment: Set(dto.comment),
|
||||
telegram_id: Set(dto.telegram_id),
|
||||
is_telegram_admin: Set(dto.is_telegram_admin),
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
@@ -103,6 +111,9 @@ impl Model {
|
||||
if dto.telegram_id.is_some() {
|
||||
active_model.telegram_id = Set(dto.telegram_id);
|
||||
}
|
||||
if let Some(is_admin) = dto.is_telegram_admin {
|
||||
active_model.is_telegram_admin = Set(is_admin);
|
||||
}
|
||||
|
||||
active_model
|
||||
}
|
||||
|
@@ -0,0 +1,51 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(TelegramConfig::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(TelegramConfig::Id)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key())
|
||||
.col(ColumnDef::new(TelegramConfig::BotToken)
|
||||
.string()
|
||||
.not_null())
|
||||
.col(ColumnDef::new(TelegramConfig::IsActive)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false))
|
||||
.col(ColumnDef::new(TelegramConfig::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null())
|
||||
.col(ColumnDef::new(TelegramConfig::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(TelegramConfig::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
pub enum TelegramConfig {
|
||||
Table,
|
||||
Id,
|
||||
BotToken,
|
||||
IsActive,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Users::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(Users::IsTelegramAdmin)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false)
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Users::Table)
|
||||
.drop_column(Users::IsTelegramAdmin)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum Users {
|
||||
Table,
|
||||
IsTelegramAdmin,
|
||||
}
|
@@ -10,6 +10,8 @@ mod m20241201_000007_create_inbound_users_table;
|
||||
mod m20250919_000001_update_inbound_users_schema;
|
||||
mod m20250922_000001_add_grpc_hostname_to_servers;
|
||||
mod m20250923_000001_create_dns_providers_table;
|
||||
mod m20250929_000001_create_telegram_config_table;
|
||||
mod m20250929_000002_add_telegram_admin_to_users;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -27,6 +29,8 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20250919_000001_update_inbound_users_schema::Migration),
|
||||
Box::new(m20250922_000001_add_grpc_hostname_to_servers::Migration),
|
||||
Box::new(m20250923_000001_create_dns_providers_table::Migration),
|
||||
Box::new(m20250929_000001_create_telegram_config_table::Migration),
|
||||
Box::new(m20250929_000002_add_telegram_admin_to_users::Migration),
|
||||
]
|
||||
}
|
||||
}
|
@@ -51,8 +51,8 @@ impl DatabaseManager {
|
||||
}
|
||||
|
||||
/// Get database connection
|
||||
pub fn connection(&self) -> &DatabaseConnection {
|
||||
&self.connection
|
||||
pub fn connection(&self) -> DatabaseConnection {
|
||||
self.connection.clone()
|
||||
}
|
||||
|
||||
/// Run database migrations
|
||||
|
@@ -6,6 +6,7 @@ pub mod server;
|
||||
pub mod server_inbound;
|
||||
pub mod user_access;
|
||||
pub mod inbound_users;
|
||||
pub mod telegram_config;
|
||||
|
||||
pub use user::UserRepository;
|
||||
pub use certificate::CertificateRepository;
|
||||
@@ -14,4 +15,5 @@ pub use inbound_template::InboundTemplateRepository;
|
||||
pub use server::ServerRepository;
|
||||
pub use server_inbound::ServerInboundRepository;
|
||||
pub use user_access::UserAccessRepository;
|
||||
pub use inbound_users::InboundUsersRepository;
|
||||
pub use inbound_users::InboundUsersRepository;
|
||||
pub use telegram_config::TelegramConfigRepository;
|
@@ -76,4 +76,13 @@ impl ServerRepository {
|
||||
|
||||
Ok(server.get_grpc_endpoint())
|
||||
}
|
||||
|
||||
pub async fn get_all(&self) -> Result<Vec<server::Model>> {
|
||||
Ok(Server::find().all(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn count(&self) -> Result<u64> {
|
||||
let count = Server::find().count(&self.db).await?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
@@ -163,4 +163,16 @@ impl ServerInboundRepository {
|
||||
|
||||
Ok(inbound.update(&self.db).await?)
|
||||
}
|
||||
|
||||
pub async fn find_by_user_id(&self, user_id: Uuid) -> Result<Vec<server_inbound::Model>> {
|
||||
// This would need a join with user_access table
|
||||
// For now, returning empty vec as placeholder
|
||||
// TODO: Implement proper join query
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
pub async fn count(&self) -> Result<u64> {
|
||||
let count = ServerInbound::find().count(&self.db).await?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
167
src/database/repository/telegram_config.rs
Normal file
167
src/database/repository/telegram_config.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use anyhow::Result;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait, ActiveModelTrait, Set, QueryFilter, ColumnTrait, QueryOrder};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::entities::telegram_config::{
|
||||
self, Model, CreateTelegramConfigDto, UpdateTelegramConfigDto
|
||||
};
|
||||
|
||||
pub struct TelegramConfigRepository {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
impl TelegramConfigRepository {
|
||||
pub fn new(db: DatabaseConnection) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
/// Get the current active configuration (should be only one)
|
||||
pub async fn get_active(&self) -> Result<Option<Model>> {
|
||||
Ok(telegram_config::Entity::find()
|
||||
.filter(telegram_config::Column::IsActive.eq(true))
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Get configuration by ID
|
||||
pub async fn find_by_id(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
Ok(telegram_config::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Get the latest configuration (active or not)
|
||||
pub async fn get_latest(&self) -> Result<Option<Model>> {
|
||||
Ok(telegram_config::Entity::find()
|
||||
.order_by_desc(telegram_config::Column::CreatedAt)
|
||||
.one(&self.db)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Create new configuration (deactivates previous if exists)
|
||||
pub async fn create(&self, dto: CreateTelegramConfigDto) -> Result<Model> {
|
||||
// If is_active is true, deactivate all other configs
|
||||
if dto.is_active {
|
||||
self.deactivate_all().await?;
|
||||
}
|
||||
|
||||
let model = telegram_config::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
bot_token: Set(dto.bot_token),
|
||||
is_active: Set(dto.is_active),
|
||||
created_at: Set(chrono::Utc::now()),
|
||||
updated_at: Set(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
Ok(model.insert(&self.db).await?)
|
||||
}
|
||||
|
||||
/// Update configuration
|
||||
pub async fn update(&self, id: Uuid, dto: UpdateTelegramConfigDto) -> Result<Option<Model>> {
|
||||
let model = telegram_config::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
let Some(model) = model else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// If activating this config, deactivate others
|
||||
if dto.is_active == Some(true) {
|
||||
self.deactivate_all_except(id).await?;
|
||||
}
|
||||
|
||||
let mut active_model = model.into_active_model();
|
||||
|
||||
if let Some(bot_token) = dto.bot_token {
|
||||
active_model.bot_token = Set(bot_token);
|
||||
}
|
||||
if let Some(is_active) = dto.is_active {
|
||||
active_model.is_active = Set(is_active);
|
||||
}
|
||||
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(Some(active_model.update(&self.db).await?))
|
||||
}
|
||||
|
||||
/// Activate a configuration (deactivates all others)
|
||||
pub async fn activate(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
self.deactivate_all_except(id).await?;
|
||||
|
||||
let model = telegram_config::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
let Some(model) = model else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut active_model = model.into_active_model();
|
||||
active_model.is_active = Set(true);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(Some(active_model.update(&self.db).await?))
|
||||
}
|
||||
|
||||
/// Deactivate a configuration
|
||||
pub async fn deactivate(&self, id: Uuid) -> Result<Option<Model>> {
|
||||
let model = telegram_config::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?;
|
||||
|
||||
let Some(model) = model else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut active_model = model.into_active_model();
|
||||
active_model.is_active = Set(false);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
Ok(Some(active_model.update(&self.db).await?))
|
||||
}
|
||||
|
||||
/// Delete configuration
|
||||
pub async fn delete(&self, id: Uuid) -> Result<bool> {
|
||||
let result = telegram_config::Entity::delete_by_id(id)
|
||||
.exec(&self.db)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
/// Deactivate all configurations
|
||||
async fn deactivate_all(&self) -> Result<()> {
|
||||
let configs = telegram_config::Entity::find()
|
||||
.filter(telegram_config::Column::IsActive.eq(true))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
for config in configs {
|
||||
let mut active_model = config.into_active_model();
|
||||
active_model.is_active = Set(false);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
active_model.update(&self.db).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deactivate all configurations except one
|
||||
async fn deactivate_all_except(&self, except_id: Uuid) -> Result<()> {
|
||||
let configs = telegram_config::Entity::find()
|
||||
.filter(telegram_config::Column::IsActive.eq(true))
|
||||
.filter(telegram_config::Column::Id.ne(except_id))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
for config in configs {
|
||||
let mut active_model = config.into_active_model();
|
||||
active_model.is_active = Set(false);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
active_model.update(&self.db).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait, QuerySelect};
|
||||
use uuid::Uuid;
|
||||
|
||||
use sea_orm::{Set, ActiveModelTrait};
|
||||
use crate::database::entities::user::{Entity as User, Column, Model, ActiveModel, CreateUserDto, UpdateUserDto};
|
||||
|
||||
pub struct UserRepository {
|
||||
@@ -124,6 +125,48 @@ impl UserRepository {
|
||||
.await?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Get all Telegram admins
|
||||
pub async fn get_telegram_admins(&self) -> Result<Vec<Model>> {
|
||||
let admins = User::find()
|
||||
.filter(Column::IsTelegramAdmin.eq(true))
|
||||
.order_by_desc(Column::CreatedAt)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(admins)
|
||||
}
|
||||
|
||||
/// Set user as Telegram admin
|
||||
pub async fn set_telegram_admin(&self, user_id: Uuid, is_admin: bool) -> Result<Option<Model>> {
|
||||
if let Some(user) = self.get_by_id(user_id).await? {
|
||||
let mut active_model: ActiveModel = user.into();
|
||||
active_model.is_telegram_admin = Set(is_admin);
|
||||
active_model.updated_at = Set(chrono::Utc::now());
|
||||
|
||||
let updated = active_model.update(&self.db).await?;
|
||||
Ok(Some(updated))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if user is Telegram admin
|
||||
pub async fn is_telegram_admin(&self, user_id: Uuid) -> Result<bool> {
|
||||
if let Some(user) = self.get_by_id(user_id).await? {
|
||||
Ok(user.is_telegram_admin)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if telegram_id is admin
|
||||
pub async fn is_telegram_id_admin(&self, telegram_id: i64) -> Result<bool> {
|
||||
if let Some(user) = self.get_by_telegram_id(telegram_id).await? {
|
||||
Ok(user.is_telegram_admin)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -158,6 +201,7 @@ mod tests {
|
||||
name: "Test User".to_string(),
|
||||
comment: Some("Test comment".to_string()),
|
||||
telegram_id: Some(123456789),
|
||||
is_telegram_admin: false,
|
||||
};
|
||||
|
||||
let created_user = repo.create(create_dto).await.unwrap();
|
||||
|
11
src/main.rs
11
src/main.rs
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
mod config;
|
||||
@@ -8,7 +9,7 @@ mod web;
|
||||
|
||||
use config::{AppConfig, args::parse_args};
|
||||
use database::DatabaseManager;
|
||||
use services::{TaskScheduler, XrayService};
|
||||
use services::{TaskScheduler, XrayService, TelegramService};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@@ -89,10 +90,16 @@ async fn main() -> Result<()> {
|
||||
// Start event-driven sync handler with the receiver
|
||||
TaskScheduler::start_event_handler(db.clone(), event_receiver).await;
|
||||
|
||||
// Initialize Telegram service if needed
|
||||
let telegram_service = Arc::new(TelegramService::new(db.clone()));
|
||||
if let Err(e) = telegram_service.initialize().await {
|
||||
tracing::warn!("Failed to initialize Telegram service: {}", e);
|
||||
}
|
||||
|
||||
// Start web server with task scheduler
|
||||
|
||||
tokio::select! {
|
||||
result = web::start_server(db, config.web.clone()) => {
|
||||
result = web::start_server(db, config.web.clone(), Some(telegram_service.clone())) => {
|
||||
match result {
|
||||
Err(e) => tracing::error!("Web server error: {}", e),
|
||||
_ => {}
|
||||
|
@@ -4,8 +4,10 @@ pub mod certificates;
|
||||
pub mod events;
|
||||
pub mod tasks;
|
||||
pub mod uri_generator;
|
||||
pub mod telegram;
|
||||
|
||||
pub use xray::XrayService;
|
||||
pub use tasks::TaskScheduler;
|
||||
pub use uri_generator::UriGeneratorService;
|
||||
pub use certificates::CertificateService;
|
||||
pub use certificates::CertificateService;
|
||||
pub use telegram::TelegramService;
|
39
src/services/telegram/bot.rs
Normal file
39
src/services/telegram/bot.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use teloxide::{Bot, prelude::*};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use super::handlers;
|
||||
|
||||
/// Run the bot polling loop
|
||||
pub async fn run_polling(
|
||||
bot: Bot,
|
||||
db: DatabaseManager,
|
||||
mut shutdown_rx: oneshot::Receiver<()>,
|
||||
) {
|
||||
tracing::info!("Starting Telegram bot polling...");
|
||||
|
||||
let handler = Update::filter_message()
|
||||
.branch(
|
||||
dptree::entry()
|
||||
.filter_command::<handlers::Command>()
|
||||
.endpoint(handlers::handle_command)
|
||||
)
|
||||
.branch(
|
||||
dptree::endpoint(handlers::handle_message)
|
||||
);
|
||||
|
||||
let mut dispatcher = Dispatcher::builder(bot.clone(), handler)
|
||||
.dependencies(dptree::deps![db])
|
||||
.enable_ctrlc_handler()
|
||||
.build();
|
||||
|
||||
// Run dispatcher with shutdown signal
|
||||
tokio::select! {
|
||||
_ = dispatcher.dispatch() => {
|
||||
tracing::info!("Telegram bot polling stopped");
|
||||
}
|
||||
_ = shutdown_rx => {
|
||||
tracing::info!("Telegram bot received shutdown signal");
|
||||
}
|
||||
}
|
||||
}
|
46
src/services/telegram/error.rs
Normal file
46
src/services/telegram/error.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TelegramError {
|
||||
#[error("Bot is not configured")]
|
||||
NotConfigured,
|
||||
|
||||
#[error("Bot is not running")]
|
||||
NotRunning,
|
||||
|
||||
#[error("Invalid bot token")]
|
||||
InvalidToken,
|
||||
|
||||
#[error("User not found")]
|
||||
UserNotFound,
|
||||
|
||||
#[error("User is not authorized")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
Database(String),
|
||||
|
||||
#[error("Telegram API error: {0}")]
|
||||
TelegramApi(String),
|
||||
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<teloxide::RequestError> for TelegramError {
|
||||
fn from(err: teloxide::RequestError) -> Self {
|
||||
Self::TelegramApi(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sea_orm::DbErr> for TelegramError {
|
||||
fn from(err: sea_orm::DbErr) -> Self {
|
||||
Self::Database(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for TelegramError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self::Other(err.to_string())
|
||||
}
|
||||
}
|
371
src/services/telegram/handlers.rs
Normal file
371
src/services/telegram/handlers.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use teloxide::{prelude::*, utils::command::BotCommands};
|
||||
use teloxide::types::Me;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::database::repository::UserRepository;
|
||||
use crate::database::entities::user::CreateUserDto;
|
||||
|
||||
/// Available bot commands
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename_rule = "lowercase", description = "Available commands:")]
|
||||
pub enum Command {
|
||||
#[command(description = "Start the bot and register")]
|
||||
Start,
|
||||
#[command(description = "Show help message")]
|
||||
Help,
|
||||
#[command(description = "Show your status")]
|
||||
Status,
|
||||
#[command(description = "List available configurations")]
|
||||
Configs,
|
||||
// Admin commands
|
||||
#[command(description = "[Admin] List all users")]
|
||||
Users,
|
||||
#[command(description = "[Admin] List all servers")]
|
||||
Servers,
|
||||
#[command(description = "[Admin] Show statistics")]
|
||||
Stats,
|
||||
#[command(description = "[Admin] Broadcast message", parse_with = "split")]
|
||||
Broadcast { message: String },
|
||||
}
|
||||
|
||||
/// Handle command messages
|
||||
pub async fn handle_command(
|
||||
bot: Bot,
|
||||
msg: Message,
|
||||
cmd: Command,
|
||||
db: DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let chat_id = msg.chat.id;
|
||||
let from = msg.from.as_ref().ok_or("No sender info")?;
|
||||
let telegram_id = from.id.0 as i64;
|
||||
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
|
||||
match cmd {
|
||||
Command::Start => {
|
||||
handle_start(bot, chat_id, telegram_id, from, &user_repo).await?;
|
||||
}
|
||||
Command::Help => {
|
||||
bot.send_message(chat_id, Command::descriptions().to_string()).await?;
|
||||
}
|
||||
Command::Status => {
|
||||
handle_status(bot, chat_id, telegram_id, &user_repo, &db).await?;
|
||||
}
|
||||
Command::Configs => {
|
||||
handle_configs(bot, chat_id, telegram_id, &user_repo, &db).await?;
|
||||
}
|
||||
// Admin commands
|
||||
Command::Users => {
|
||||
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
|
||||
return Ok(());
|
||||
}
|
||||
handle_users(bot, chat_id, &user_repo).await?;
|
||||
}
|
||||
Command::Servers => {
|
||||
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
|
||||
return Ok(());
|
||||
}
|
||||
handle_servers(bot, chat_id, &db).await?;
|
||||
}
|
||||
Command::Stats => {
|
||||
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
|
||||
return Ok(());
|
||||
}
|
||||
handle_stats(bot, chat_id, &db).await?;
|
||||
}
|
||||
Command::Broadcast { message } => {
|
||||
if !user_repo.is_telegram_id_admin(telegram_id).await.unwrap_or(false) {
|
||||
bot.send_message(chat_id, "❌ You are not authorized to use this command").await?;
|
||||
return Ok(());
|
||||
}
|
||||
handle_broadcast(bot, chat_id, message, &user_repo).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle regular text messages
|
||||
pub async fn handle_message(
|
||||
bot: Bot,
|
||||
msg: Message,
|
||||
db: DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Some(text) = msg.text() {
|
||||
if !text.starts_with('/') {
|
||||
bot.send_message(
|
||||
msg.chat.id,
|
||||
"Please use /help to see available commands"
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /start command
|
||||
async fn handle_start(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
telegram_id: i64,
|
||||
from: &teloxide::types::User,
|
||||
user_repo: &UserRepository,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Check if user already exists
|
||||
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
let message = format!(
|
||||
"👋 Welcome back, {}!\n\n\
|
||||
You are already registered.\n\
|
||||
Use /help to see available commands.",
|
||||
user.name
|
||||
);
|
||||
bot.send_message(chat_id, message).await?;
|
||||
} else {
|
||||
// Create new user
|
||||
let username = from.username.as_deref().unwrap_or("Unknown");
|
||||
let full_name = format!(
|
||||
"{} {}",
|
||||
from.first_name,
|
||||
from.last_name.as_deref().unwrap_or("")
|
||||
).trim().to_string();
|
||||
|
||||
let dto = CreateUserDto {
|
||||
name: if !full_name.is_empty() { full_name } else { username.to_string() },
|
||||
comment: Some(format!("Telegram user: @{}", username)),
|
||||
telegram_id: Some(telegram_id),
|
||||
is_telegram_admin: false,
|
||||
};
|
||||
|
||||
match user_repo.create(dto).await {
|
||||
Ok(user) => {
|
||||
let message = format!(
|
||||
"✅ Registration successful!\n\n\
|
||||
Name: {}\n\
|
||||
User ID: {}\n\n\
|
||||
Use /help to see available commands.",
|
||||
user.name, user.id
|
||||
);
|
||||
bot.send_message(chat_id, message).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
format!("❌ Registration failed: {}", e)
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /status command
|
||||
async fn handle_status(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
telegram_id: i64,
|
||||
user_repo: &UserRepository,
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
let server_inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
|
||||
let configs = server_inbound_repo.find_by_user_id(user.id).await.unwrap_or_default();
|
||||
|
||||
let admin_status = if user.is_telegram_admin { "Admin" } else { "User" };
|
||||
|
||||
let message = format!(
|
||||
"📊 Your Status\n\n\
|
||||
Name: {}\n\
|
||||
User ID: {}\n\
|
||||
Role: {}\n\
|
||||
Active Configs: {}\n\
|
||||
Registered: {}",
|
||||
user.name,
|
||||
user.id,
|
||||
admin_status,
|
||||
configs.len(),
|
||||
user.created_at.format("%Y-%m-%d %H:%M UTC")
|
||||
);
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
} else {
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
"❌ You are not registered. Use /start to register."
|
||||
).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /configs command
|
||||
async fn handle_configs(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
telegram_id: i64,
|
||||
user_repo: &UserRepository,
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Some(user) = user_repo.get_by_telegram_id(telegram_id).await.unwrap_or(None) {
|
||||
let server_inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
|
||||
let configs = server_inbound_repo.find_by_user_id(user.id).await.unwrap_or_default();
|
||||
|
||||
if configs.is_empty() {
|
||||
bot.send_message(chat_id, "You don't have any configurations yet.").await?;
|
||||
} else {
|
||||
let mut message = String::from("📋 Your Configurations:\n\n");
|
||||
|
||||
for (i, config) in configs.iter().enumerate() {
|
||||
message.push_str(&format!(
|
||||
"{}. {} (Port: {})\n",
|
||||
i + 1,
|
||||
config.tag,
|
||||
config.port_override.unwrap_or(0)
|
||||
));
|
||||
}
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
}
|
||||
} else {
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
"❌ You are not registered. Use /start to register."
|
||||
).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /users command (admin only)
|
||||
async fn handle_users(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
user_repo: &UserRepository,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let users = user_repo.get_all(1, 100).await.unwrap_or_default();
|
||||
|
||||
if users.is_empty() {
|
||||
bot.send_message(chat_id, "No users found.").await?;
|
||||
} else {
|
||||
let mut message = String::from("👥 Users:\n\n");
|
||||
|
||||
for (i, user) in users.iter().enumerate() {
|
||||
let telegram_status = if user.telegram_id.is_some() { "✅" } else { "❌" };
|
||||
let admin_status = if user.is_telegram_admin { " (Admin)" } else { "" };
|
||||
|
||||
message.push_str(&format!(
|
||||
"{}. {} {} {}{}\n",
|
||||
i + 1,
|
||||
user.name,
|
||||
telegram_status,
|
||||
user.id,
|
||||
admin_status
|
||||
));
|
||||
}
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /servers command (admin only)
|
||||
async fn handle_servers(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let server_repo = crate::database::repository::ServerRepository::new(db.connection());
|
||||
let servers = server_repo.get_all().await.unwrap_or_default();
|
||||
|
||||
if servers.is_empty() {
|
||||
bot.send_message(chat_id, "No servers found.").await?;
|
||||
} else {
|
||||
let mut message = String::from("🖥️ Servers:\n\n");
|
||||
|
||||
for (i, server) in servers.iter().enumerate() {
|
||||
let status = if server.status == "active" { "✅" } else { "❌" };
|
||||
|
||||
message.push_str(&format!(
|
||||
"{}. {} {} - {}\n",
|
||||
i + 1,
|
||||
status,
|
||||
server.name,
|
||||
server.hostname
|
||||
));
|
||||
}
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /stats command (admin only)
|
||||
async fn handle_stats(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
db: &DatabaseManager,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let user_repo = UserRepository::new(db.connection());
|
||||
let server_repo = crate::database::repository::ServerRepository::new(db.connection());
|
||||
let inbound_repo = crate::database::repository::ServerInboundRepository::new(db.connection());
|
||||
|
||||
let user_count = user_repo.count().await.unwrap_or(0);
|
||||
let server_count = server_repo.count().await.unwrap_or(0);
|
||||
let inbound_count = inbound_repo.count().await.unwrap_or(0);
|
||||
|
||||
let message = format!(
|
||||
"📊 Statistics\n\n\
|
||||
Total Users: {}\n\
|
||||
Total Servers: {}\n\
|
||||
Total Inbounds: {}",
|
||||
user_count,
|
||||
server_count,
|
||||
inbound_count
|
||||
);
|
||||
|
||||
bot.send_message(chat_id, message).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle /broadcast command (admin only)
|
||||
async fn handle_broadcast(
|
||||
bot: Bot,
|
||||
chat_id: ChatId,
|
||||
message: String,
|
||||
user_repo: &UserRepository,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let users = user_repo.get_all(1, 1000).await.unwrap_or_default();
|
||||
let mut sent_count = 0;
|
||||
let mut failed_count = 0;
|
||||
|
||||
for user in users {
|
||||
if let Some(telegram_id) = user.telegram_id {
|
||||
match bot.send_message(ChatId(telegram_id), &message).await {
|
||||
Ok(_) => sent_count += 1,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to send broadcast to {}: {}", telegram_id, e);
|
||||
failed_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
format!(
|
||||
"✅ Broadcast complete\n\
|
||||
Sent: {}\n\
|
||||
Failed: {}",
|
||||
sent_count, failed_count
|
||||
)
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
175
src/services/telegram/mod.rs
Normal file
175
src/services/telegram/mod.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use teloxide::{Bot, prelude::*};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::database::repository::TelegramConfigRepository;
|
||||
use crate::database::entities::telegram_config::Model as TelegramConfig;
|
||||
|
||||
pub mod bot;
|
||||
pub mod handlers;
|
||||
pub mod error;
|
||||
|
||||
pub use error::TelegramError;
|
||||
|
||||
/// Main Telegram service that manages the bot lifecycle
|
||||
pub struct TelegramService {
|
||||
db: DatabaseManager,
|
||||
bot: Arc<RwLock<Option<Bot>>>,
|
||||
config: Arc<RwLock<Option<TelegramConfig>>>,
|
||||
shutdown_signal: Arc<RwLock<Option<tokio::sync::oneshot::Sender<()>>>>,
|
||||
}
|
||||
|
||||
impl TelegramService {
|
||||
/// Create a new Telegram service
|
||||
pub fn new(db: DatabaseManager) -> Self {
|
||||
Self {
|
||||
db,
|
||||
bot: Arc::new(RwLock::new(None)),
|
||||
config: Arc::new(RwLock::new(None)),
|
||||
shutdown_signal: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize and start the bot if active configuration exists
|
||||
pub async fn initialize(&self) -> Result<()> {
|
||||
let repo = TelegramConfigRepository::new(self.db.connection());
|
||||
|
||||
// Get active configuration
|
||||
if let Some(config) = repo.get_active().await? {
|
||||
self.start_with_config(config).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start bot with specific configuration
|
||||
pub async fn start_with_config(&self, config: TelegramConfig) -> Result<()> {
|
||||
// Stop existing bot if running
|
||||
self.stop().await?;
|
||||
|
||||
// Create new bot instance
|
||||
let bot = Bot::new(&config.bot_token);
|
||||
|
||||
// Verify token by calling getMe
|
||||
match bot.get_me().await {
|
||||
Ok(me) => {
|
||||
let username = me.user.username.unwrap_or_default();
|
||||
tracing::info!("Telegram bot started: @{}", username);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!("Invalid bot token: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// Store bot and config
|
||||
*self.bot.write().await = Some(bot.clone());
|
||||
*self.config.write().await = Some(config.clone());
|
||||
|
||||
// Start polling in background
|
||||
if config.is_active {
|
||||
self.start_polling(bot).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start polling for updates
|
||||
async fn start_polling(&self, bot: Bot) -> Result<()> {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
*self.shutdown_signal.write().await = Some(tx);
|
||||
|
||||
let db = self.db.clone();
|
||||
|
||||
// Spawn polling task
|
||||
tokio::spawn(async move {
|
||||
bot::run_polling(bot, db, rx).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the bot
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
// Send shutdown signal if polling is running
|
||||
if let Some(tx) = self.shutdown_signal.write().await.take() {
|
||||
let _ = tx.send(()); // Ignore error if receiver is already dropped
|
||||
}
|
||||
|
||||
// Clear bot and config
|
||||
*self.bot.write().await = None;
|
||||
*self.config.write().await = None;
|
||||
|
||||
tracing::info!("Telegram bot stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update configuration and restart if needed
|
||||
pub async fn update_config(&self, config_id: Uuid) -> Result<()> {
|
||||
let repo = TelegramConfigRepository::new(self.db.connection());
|
||||
|
||||
if let Some(config) = repo.find_by_id(config_id).await? {
|
||||
if config.is_active {
|
||||
self.start_with_config(config).await?;
|
||||
} else {
|
||||
self.stop().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current bot status
|
||||
pub async fn get_status(&self) -> BotStatus {
|
||||
let bot_guard = self.bot.read().await;
|
||||
let config_guard = self.config.read().await;
|
||||
|
||||
BotStatus {
|
||||
is_running: bot_guard.is_some(),
|
||||
config: config_guard.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send message to user
|
||||
pub async fn send_message(&self, chat_id: i64, text: String) -> Result<()> {
|
||||
let bot_guard = self.bot.read().await;
|
||||
|
||||
if let Some(bot) = bot_guard.as_ref() {
|
||||
bot.send_message(ChatId(chat_id), text).await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Bot is not running"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send message to all admins
|
||||
pub async fn broadcast_to_admins(&self, text: String) -> Result<()> {
|
||||
let bot_guard = self.bot.read().await;
|
||||
|
||||
if let Some(bot) = bot_guard.as_ref() {
|
||||
let user_repo = crate::database::repository::UserRepository::new(self.db.connection());
|
||||
let admins = user_repo.get_telegram_admins().await?;
|
||||
|
||||
for admin in admins {
|
||||
if let Some(telegram_id) = admin.telegram_id {
|
||||
if let Err(e) = bot.send_message(ChatId(telegram_id), text.clone()).await {
|
||||
tracing::warn!("Failed to send message to admin {}: {}", telegram_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Bot is not running"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bot status information
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct BotStatus {
|
||||
pub is_running: bool,
|
||||
pub config: Option<TelegramConfig>,
|
||||
}
|
@@ -5,6 +5,7 @@ pub mod templates;
|
||||
pub mod client_configs;
|
||||
pub mod dns_providers;
|
||||
pub mod tasks;
|
||||
pub mod telegram;
|
||||
|
||||
pub use users::*;
|
||||
pub use servers::*;
|
||||
@@ -12,4 +13,5 @@ pub use certificates::*;
|
||||
pub use templates::*;
|
||||
pub use client_configs::*;
|
||||
pub use dns_providers::*;
|
||||
pub use tasks::*;
|
||||
pub use tasks::*;
|
||||
pub use telegram::*;
|
@@ -504,6 +504,7 @@ pub async fn add_user_to_inbound(
|
||||
name: user_name.clone(),
|
||||
comment: user_data["comment"].as_str().map(|s| s.to_string()),
|
||||
telegram_id: user_data["telegram_id"].as_i64(),
|
||||
is_telegram_admin: false,
|
||||
};
|
||||
|
||||
match user_repo.create(create_user_dto).await {
|
||||
|
304
src/web/handlers/telegram.rs
Normal file
304
src/web/handlers/telegram.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use axum::{
|
||||
extract::{State, Path, Json},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::web::AppState;
|
||||
use crate::database::repository::{UserRepository, TelegramConfigRepository};
|
||||
use crate::database::entities::telegram_config::{CreateTelegramConfigDto, UpdateTelegramConfigDto};
|
||||
|
||||
/// Response for Telegram config
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TelegramConfigResponse {
|
||||
pub id: Uuid,
|
||||
pub is_active: bool,
|
||||
pub bot_info: Option<BotInfo>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BotInfo {
|
||||
pub username: String,
|
||||
pub first_name: String,
|
||||
}
|
||||
|
||||
/// Get current Telegram configuration
|
||||
pub async fn get_telegram_config(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = TelegramConfigRepository::new(state.db.connection());
|
||||
|
||||
match repo.get_latest().await {
|
||||
Ok(Some(config)) => {
|
||||
let mut response = TelegramConfigResponse {
|
||||
id: config.id,
|
||||
is_active: config.is_active,
|
||||
bot_info: None,
|
||||
created_at: config.created_at.to_rfc3339(),
|
||||
updated_at: config.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
// Get bot info if active
|
||||
if config.is_active {
|
||||
if let Ok(status) = get_bot_status(&state).await {
|
||||
response.bot_info = status.bot_info;
|
||||
}
|
||||
}
|
||||
|
||||
Json(response).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get telegram config: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new Telegram configuration
|
||||
pub async fn create_telegram_config(
|
||||
State(state): State<AppState>,
|
||||
Json(dto): Json<CreateTelegramConfigDto>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = TelegramConfigRepository::new(state.db.connection());
|
||||
|
||||
match repo.create(dto).await {
|
||||
Ok(config) => {
|
||||
// Initialize telegram service with new config if active
|
||||
if config.is_active {
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
let _ = telegram_service.update_config(config.id).await;
|
||||
}
|
||||
}
|
||||
|
||||
(StatusCode::CREATED, Json(config)).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create telegram config: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update Telegram configuration
|
||||
pub async fn update_telegram_config(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(dto): Json<UpdateTelegramConfigDto>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = TelegramConfigRepository::new(state.db.connection());
|
||||
|
||||
match repo.update(id, dto).await {
|
||||
Ok(Some(config)) => {
|
||||
// Update telegram service
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
let _ = telegram_service.update_config(config.id).await;
|
||||
}
|
||||
|
||||
Json(config).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to update telegram config: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete Telegram configuration
|
||||
pub async fn delete_telegram_config(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = TelegramConfigRepository::new(state.db.connection());
|
||||
|
||||
// Stop bot if this config is active
|
||||
if let Ok(Some(config)) = repo.find_by_id(id).await {
|
||||
if config.is_active {
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
let _ = telegram_service.stop().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match repo.delete(id).await {
|
||||
Ok(true) => StatusCode::NO_CONTENT.into_response(),
|
||||
Ok(false) => StatusCode::NOT_FOUND.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to delete telegram config: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Telegram bot status
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BotStatusResponse {
|
||||
pub is_running: bool,
|
||||
pub bot_info: Option<BotInfo>,
|
||||
}
|
||||
|
||||
async fn get_bot_status(state: &AppState) -> Result<BotStatusResponse, String> {
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
let status = telegram_service.get_status().await;
|
||||
|
||||
let bot_info = if status.is_running {
|
||||
// In production, you would get this from the bot API
|
||||
Some(BotInfo {
|
||||
username: "bot".to_string(),
|
||||
first_name: "Bot".to_string(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(BotStatusResponse {
|
||||
is_running: status.is_running,
|
||||
bot_info,
|
||||
})
|
||||
} else {
|
||||
Ok(BotStatusResponse {
|
||||
is_running: false,
|
||||
bot_info: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_telegram_status(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
match get_bot_status(&state).await {
|
||||
Ok(status) => Json(status).into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get bot status: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of Telegram admins
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TelegramAdmin {
|
||||
pub user_id: Uuid,
|
||||
pub name: String,
|
||||
pub telegram_id: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn get_telegram_admins(
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = UserRepository::new(state.db.connection());
|
||||
|
||||
match repo.get_telegram_admins().await {
|
||||
Ok(admins) => {
|
||||
let response: Vec<TelegramAdmin> = admins
|
||||
.into_iter()
|
||||
.map(|u| TelegramAdmin {
|
||||
user_id: u.id,
|
||||
name: u.name,
|
||||
telegram_id: u.telegram_id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(response).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get telegram admins: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add Telegram admin
|
||||
pub async fn add_telegram_admin(
|
||||
State(state): State<AppState>,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = UserRepository::new(state.db.connection());
|
||||
|
||||
match repo.set_telegram_admin(user_id, true).await {
|
||||
Ok(Some(user)) => {
|
||||
// Notify via Telegram if bot is running
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
if let Some(telegram_id) = user.telegram_id {
|
||||
let _ = telegram_service.send_message(
|
||||
telegram_id,
|
||||
"✅ You have been granted admin privileges!".to_string()
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
Json(user).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to add telegram admin: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove Telegram admin
|
||||
pub async fn remove_telegram_admin(
|
||||
State(state): State<AppState>,
|
||||
Path(user_id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let repo = UserRepository::new(state.db.connection());
|
||||
|
||||
match repo.set_telegram_admin(user_id, false).await {
|
||||
Ok(Some(user)) => {
|
||||
// Notify via Telegram if bot is running
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
if let Some(telegram_id) = user.telegram_id {
|
||||
let _ = telegram_service.send_message(
|
||||
telegram_id,
|
||||
"❌ Your admin privileges have been revoked.".to_string()
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
Json(user).into_response()
|
||||
}
|
||||
Ok(None) => {
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to remove telegram admin: {}", e);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send test message
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SendMessageRequest {
|
||||
pub chat_id: i64,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub async fn send_test_message(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<SendMessageRequest>,
|
||||
) -> impl IntoResponse {
|
||||
if let Some(telegram_service) = &state.telegram_service {
|
||||
match telegram_service.send_message(req.chat_id, req.text).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to send test message: {}", e);
|
||||
(StatusCode::BAD_REQUEST, e.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StatusCode::SERVICE_UNAVAILABLE.into_response()
|
||||
}
|
||||
}
|
@@ -13,9 +13,10 @@ use tower_http::cors::CorsLayer;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing::info;
|
||||
|
||||
use std::sync::Arc;
|
||||
use crate::config::WebConfig;
|
||||
use crate::database::DatabaseManager;
|
||||
use crate::services::XrayService;
|
||||
use crate::services::{XrayService, TelegramService};
|
||||
|
||||
pub mod handlers;
|
||||
pub mod routes;
|
||||
@@ -29,16 +30,18 @@ pub struct AppState {
|
||||
#[allow(dead_code)]
|
||||
pub config: WebConfig,
|
||||
pub xray_service: XrayService,
|
||||
pub telegram_service: Option<Arc<TelegramService>>,
|
||||
}
|
||||
|
||||
/// Start the web server
|
||||
pub async fn start_server(db: DatabaseManager, config: WebConfig) -> Result<()> {
|
||||
pub async fn start_server(db: DatabaseManager, config: WebConfig, telegram_service: Option<Arc<TelegramService>>) -> Result<()> {
|
||||
let xray_service = XrayService::new();
|
||||
|
||||
let app_state = AppState {
|
||||
db,
|
||||
config: config.clone(),
|
||||
xray_service,
|
||||
telegram_service,
|
||||
};
|
||||
|
||||
// Serve static files
|
||||
|
@@ -16,6 +16,7 @@ pub fn api_routes() -> Router<AppState> {
|
||||
.nest("/templates", servers::template_routes())
|
||||
.nest("/dns-providers", dns_provider_routes())
|
||||
.nest("/tasks", task_routes())
|
||||
.nest("/telegram", telegram_routes())
|
||||
}
|
||||
|
||||
/// User management routes
|
||||
@@ -46,4 +47,21 @@ fn task_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(handlers::get_tasks_status))
|
||||
.route("/:id/trigger", post(handlers::trigger_task))
|
||||
}
|
||||
|
||||
/// Telegram bot management routes
|
||||
fn telegram_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/config", get(handlers::get_telegram_config)
|
||||
.post(handlers::create_telegram_config))
|
||||
.route("/config/:id",
|
||||
get(handlers::get_telegram_config)
|
||||
.put(handlers::update_telegram_config)
|
||||
.delete(handlers::delete_telegram_config))
|
||||
.route("/status", get(handlers::get_telegram_status))
|
||||
.route("/admins", get(handlers::get_telegram_admins))
|
||||
.route("/admins/:user_id",
|
||||
post(handlers::add_telegram_admin)
|
||||
.delete(handlers::remove_telegram_admin))
|
||||
.route("/send", post(handlers::send_test_message))
|
||||
}
|
@@ -746,6 +746,166 @@
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Telegram Bot Styles */
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-dot.status-active {
|
||||
background: #34c759;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-dot.status-inactive {
|
||||
background: #8e8e93;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(52, 199, 89, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(52, 199, 89, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(52, 199, 89, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-input-group .form-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-input-group .button {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.form-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.admin-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.admin-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.admin-name {
|
||||
font-weight: 500;
|
||||
color: #1d1d1f;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.admin-telegram-id {
|
||||
font-size: 12px;
|
||||
color: #6e6e73;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.user-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #1d1d1f;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.user-telegram-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #6e6e73;
|
||||
}
|
||||
|
||||
.telegram-connected {
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.telegram-not-connected {
|
||||
color: #8e8e93;
|
||||
}
|
||||
|
||||
.bot-info {
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.bot-info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bot-info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bot-info-label {
|
||||
font-weight: 500;
|
||||
color: #6e6e73;
|
||||
}
|
||||
|
||||
.bot-info-value {
|
||||
color: #1d1d1f;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -778,7 +938,7 @@
|
||||
<a href="#users" class="nav-link" onclick="showPage('users')">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#tasks" class="nav-link" onclick="showPage('tasks')">Tasks</a>
|
||||
<a href="#telegram" class="nav-link" onclick="showPage('telegram')">Telegram Bot</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -1007,6 +1167,132 @@
|
||||
<div id="tasksTable" class="loading">Loading...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="telegram" class="page-section">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Telegram Bot</h1>
|
||||
<p class="page-subtitle">Configure and manage Telegram bot integration</p>
|
||||
</div>
|
||||
|
||||
<!-- Bot Status Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Bot Status</h2>
|
||||
<div id="botStatusIndicator" class="status-indicator">
|
||||
<span class="status-dot status-inactive"></span>
|
||||
<span class="status-text">Inactive</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="botStatusInfo" class="loading">Loading...</div>
|
||||
</div>
|
||||
|
||||
<!-- Bot Configuration Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Configuration</h2>
|
||||
<div class="card-actions">
|
||||
<button id="saveConfigBtn" class="button button-primary" onclick="saveTelegramConfig()" disabled>
|
||||
<span class="icon">💾</span>
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="telegramConfigForm" class="form">
|
||||
<div class="form-group">
|
||||
<label for="botToken" class="form-label">Bot Token</label>
|
||||
<div class="form-input-group">
|
||||
<input type="password" id="botToken" class="form-input" placeholder="Enter bot token from @BotFather">
|
||||
<button type="button" class="button button-outline" onclick="toggleTokenVisibility()">
|
||||
<span class="icon">👁</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-help">
|
||||
Get your bot token from @BotFather on Telegram
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" id="botActive" onchange="onBotActiveChange()">
|
||||
<span class="checkmark"></span>
|
||||
Enable Bot
|
||||
</label>
|
||||
<div class="form-help">
|
||||
When enabled, bot will start polling for messages
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admins Management Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Bot Administrators</h2>
|
||||
<button class="button button-outline" onclick="refreshAdmins()">
|
||||
<span class="icon">🔄</span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div id="adminsTable" class="loading">Loading...</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Management Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Admin Management</h2>
|
||||
<div class="form-input-group">
|
||||
<input type="text" id="userSearchInput" class="form-input" placeholder="Search users by name, ID, or Telegram ID" style="min-width: 300px;">
|
||||
<button class="button button-outline" onclick="searchUsers()">
|
||||
<span class="icon">🔍</span>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Search for users and manage admin privileges. Only users connected to Telegram can be promoted to admin.</p>
|
||||
<div id="userSearchResults"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users List Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">All Users</h2>
|
||||
<button class="button button-outline" onclick="refreshTelegramUsers()">
|
||||
<span class="icon">🔄</span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div id="telegramUsersTable" class="loading">Loading...</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Message Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Send Test Message</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="testMessageForm" class="form">
|
||||
<div class="form-group">
|
||||
<label for="testChatId" class="form-label">Chat ID</label>
|
||||
<input type="number" id="testChatId" class="form-input" placeholder="Enter chat ID">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="testMessage" class="form-label">Message</label>
|
||||
<textarea id="testMessage" class="form-input" rows="3" placeholder="Enter test message"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button button-primary">
|
||||
<span class="icon">📤</span>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -2268,6 +2554,428 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Telegram Bot Functions
|
||||
let currentTelegramConfig = null;
|
||||
|
||||
async function loadTelegram() {
|
||||
await loadBotStatus();
|
||||
await loadTelegramConfig();
|
||||
await loadAdmins();
|
||||
await loadTelegramUsers();
|
||||
}
|
||||
|
||||
async function loadBotStatus() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/telegram/status`);
|
||||
if (response.ok) {
|
||||
const status = await response.json();
|
||||
updateBotStatusUI(status);
|
||||
} else {
|
||||
updateBotStatusUI({ is_running: false, bot_info: null });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading bot status:', error);
|
||||
updateBotStatusUI({ is_running: false, bot_info: null });
|
||||
}
|
||||
}
|
||||
|
||||
function updateBotStatusUI(status) {
|
||||
const indicator = document.getElementById('botStatusIndicator');
|
||||
const statusInfo = document.getElementById('botStatusInfo');
|
||||
|
||||
const dot = indicator.querySelector('.status-dot');
|
||||
const text = indicator.querySelector('.status-text');
|
||||
|
||||
if (status.is_running) {
|
||||
dot.className = 'status-dot status-active';
|
||||
text.textContent = 'Active';
|
||||
|
||||
if (status.bot_info) {
|
||||
statusInfo.innerHTML = `
|
||||
<div class="bot-info">
|
||||
<div class="bot-info-item">
|
||||
<span class="bot-info-label">Username:</span>
|
||||
<span class="bot-info-value">@${status.bot_info.username}</span>
|
||||
</div>
|
||||
<div class="bot-info-item">
|
||||
<span class="bot-info-label">Name:</span>
|
||||
<span class="bot-info-value">${status.bot_info.first_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
dot.className = 'status-dot status-inactive';
|
||||
text.textContent = 'Inactive';
|
||||
statusInfo.innerHTML = '<p class="empty-state-text">Bot is not running</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTelegramConfig() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/telegram/config`);
|
||||
if (response.ok) {
|
||||
currentTelegramConfig = await response.json();
|
||||
updateConfigForm(currentTelegramConfig);
|
||||
} else if (response.status === 404) {
|
||||
currentTelegramConfig = null;
|
||||
updateConfigForm(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading config:', error);
|
||||
currentTelegramConfig = null;
|
||||
updateConfigForm(null);
|
||||
}
|
||||
}
|
||||
|
||||
function updateConfigForm(config) {
|
||||
const botTokenInput = document.getElementById('botToken');
|
||||
const botActiveCheckbox = document.getElementById('botActive');
|
||||
const saveBtn = document.getElementById('saveConfigBtn');
|
||||
|
||||
if (config) {
|
||||
botTokenInput.value = '••••••••••••••••'; // Masked token
|
||||
botActiveCheckbox.checked = config.is_active;
|
||||
} else {
|
||||
botTokenInput.value = '';
|
||||
botActiveCheckbox.checked = false;
|
||||
}
|
||||
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
|
||||
function toggleTokenVisibility() {
|
||||
const tokenInput = document.getElementById('botToken');
|
||||
const button = event.target.closest('button');
|
||||
|
||||
if (tokenInput.type === 'password') {
|
||||
tokenInput.type = 'text';
|
||||
button.innerHTML = '<span class="icon">🙈</span>';
|
||||
} else {
|
||||
tokenInput.type = 'password';
|
||||
button.innerHTML = '<span class="icon">👁</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function onBotActiveChange() {
|
||||
const checkbox = document.getElementById('botActive');
|
||||
const tokenInput = document.getElementById('botToken');
|
||||
|
||||
if (checkbox.checked && !tokenInput.value) {
|
||||
showAlert('Please enter a bot token first', 'warning');
|
||||
checkbox.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTelegramConfig() {
|
||||
const botToken = document.getElementById('botToken').value;
|
||||
const isActive = document.getElementById('botActive').checked;
|
||||
const saveBtn = document.getElementById('saveConfigBtn');
|
||||
|
||||
if (!botToken || botToken === '••••••••••••••••') {
|
||||
showAlert('Please enter a valid bot token', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const method = currentTelegramConfig ? 'PUT' : 'POST';
|
||||
const url = currentTelegramConfig ?
|
||||
`${API_BASE}/telegram/config/${currentTelegramConfig.id}` :
|
||||
`${API_BASE}/telegram/config`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bot_token: botToken,
|
||||
is_active: isActive
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Configuration saved successfully', 'success');
|
||||
await loadTelegramConfig();
|
||||
await loadBotStatus();
|
||||
} else {
|
||||
const error = await response.text();
|
||||
showAlert('Error saving configuration: ' + error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Error saving configuration: ' + error.message, 'error');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<span class="icon">💾</span> Save Configuration';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAdmins() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/telegram/admins`);
|
||||
if (response.ok) {
|
||||
const admins = await response.json();
|
||||
renderAdmins(admins);
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('adminsTable').innerHTML = '<div class="empty-state"><h3>Error loading admins</h3></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderAdmins(admins) {
|
||||
const container = document.getElementById('adminsTable');
|
||||
|
||||
if (admins.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><h3>No administrators</h3><p>Add administrators to manage the bot</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const adminsHtml = admins.map(admin => `
|
||||
<div class="admin-item">
|
||||
<div class="admin-info">
|
||||
<h4 class="admin-name">${admin.name}</h4>
|
||||
<div class="admin-telegram-id">${admin.telegram_id ? `ID: ${admin.telegram_id}` : 'No Telegram ID'}</div>
|
||||
</div>
|
||||
<div class="admin-actions">
|
||||
<button class="button button-outline button-small" onclick="removeAdmin('${admin.user_id}')">
|
||||
<span class="icon">❌</span>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = `<div class="admins-list">${adminsHtml}</div>`;
|
||||
}
|
||||
|
||||
async function loadTelegramUsers() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/users`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const users = data.users || data; // Handle both paginated and direct array responses
|
||||
renderTelegramUsers(users);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error('Error response:', response.status, errorText);
|
||||
document.getElementById('telegramUsersTable').innerHTML =
|
||||
`<div class="empty-state"><h3>Error loading users</h3><p>Status: ${response.status}</p><p>${errorText}</p></div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
document.getElementById('telegramUsersTable').innerHTML =
|
||||
`<div class="empty-state"><h3>Error loading users</h3><p>Network error: ${error.message}</p></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTelegramUsers(users) {
|
||||
const container = document.getElementById('telegramUsersTable');
|
||||
|
||||
if (users.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><h3>No users</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const usersHtml = users.map(user => `
|
||||
<div class="user-item">
|
||||
<div class="user-info">
|
||||
<h4 class="user-name">${user.name}</h4>
|
||||
<div class="user-telegram-status">
|
||||
${user.telegram_id ?
|
||||
`<span class="telegram-connected">📱 Connected (ID: ${user.telegram_id})</span>` :
|
||||
'<span class="telegram-not-connected">📱 Not connected</span>'
|
||||
}
|
||||
${user.is_telegram_admin ? '<span class="admin-badge">👑 Admin</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
${user.telegram_id && !user.is_telegram_admin ?
|
||||
`<button class="button button-primary button-small" onclick="makeAdmin('${user.id}')">
|
||||
<span class="icon">👑</span>
|
||||
Make Admin
|
||||
</button>` : ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = `<div class="users-list">${usersHtml}</div>`;
|
||||
}
|
||||
|
||||
async function makeAdmin(userId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/telegram/admins/${userId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('User promoted to admin', 'success');
|
||||
await loadAdmins();
|
||||
await loadTelegramUsers();
|
||||
} else {
|
||||
showAlert('Error promoting user to admin', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Error promoting user: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAdmin(userId) {
|
||||
if (!confirm('Are you sure you want to remove admin privileges?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/telegram/admins/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Admin privileges removed', 'success');
|
||||
await loadAdmins();
|
||||
await loadTelegramUsers();
|
||||
} else {
|
||||
showAlert('Error removing admin privileges', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Error removing admin: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAdmins() {
|
||||
await loadAdmins();
|
||||
}
|
||||
|
||||
async function refreshTelegramUsers() {
|
||||
await loadTelegramUsers();
|
||||
}
|
||||
|
||||
async function searchUsers() {
|
||||
const query = document.getElementById('userSearchInput').value.trim();
|
||||
if (!query) {
|
||||
document.getElementById('userSearchResults').innerHTML = '<p class="text-muted">Enter a search term to find users</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/users/search?q=${encodeURIComponent(query)}`);
|
||||
if (response.ok) {
|
||||
const users = await response.json();
|
||||
renderSearchResults(users);
|
||||
} else {
|
||||
document.getElementById('userSearchResults').innerHTML = '<div class="empty-state"><h3>Error searching users</h3></div>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('userSearchResults').innerHTML = '<div class="empty-state"><h3>Search failed</h3></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSearchResults(users) {
|
||||
const container = document.getElementById('userSearchResults');
|
||||
|
||||
if (users.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><h3>No users found</h3><p>Try a different search term</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const usersHtml = users.map(user => `
|
||||
<div class="user-item" style="border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-bottom: 12px;">
|
||||
<div class="user-info">
|
||||
<h4 class="user-name">${user.name}</h4>
|
||||
<div class="user-details" style="margin-top: 8px;">
|
||||
<p style="margin: 4px 0; font-size: 14px; color: #6b7280;">ID: ${user.id}</p>
|
||||
${user.telegram_id ?
|
||||
`<p style="margin: 4px 0; font-size: 14px; color: #6b7280;">📱 Telegram ID: ${user.telegram_id}</p>` :
|
||||
'<p style="margin: 4px 0; font-size: 14px; color: #ef4444;">📱 Not connected to Telegram</p>'
|
||||
}
|
||||
${user.is_telegram_admin ?
|
||||
'<p style="margin: 4px 0; font-size: 14px; color: #059669;">👑 Current Admin</p>' :
|
||||
'<p style="margin: 4px 0; font-size: 14px; color: #6b7280;">Regular User</p>'
|
||||
}
|
||||
${user.comment ? `<p style="margin: 4px 0; font-size: 14px; color: #6b7280;">Comment: ${user.comment}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-actions" style="margin-top: 12px;">
|
||||
${user.telegram_id && !user.is_telegram_admin ?
|
||||
`<button class="button button-primary" onclick="makeAdmin('${user.id}')" style="margin-right: 8px;">
|
||||
<span class="icon">👑</span>
|
||||
Make Admin
|
||||
</button>` : ''
|
||||
}
|
||||
${user.telegram_id && user.is_telegram_admin ?
|
||||
`<button class="button button-danger" onclick="removeAdmin('${user.id}')">
|
||||
<span class="icon">👑</span>
|
||||
Remove Admin
|
||||
</button>` : ''
|
||||
}
|
||||
${!user.telegram_id ?
|
||||
'<span style="color: #6b7280; font-size: 14px;">User must connect to Telegram first</span>' : ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = usersHtml;
|
||||
}
|
||||
|
||||
// Add Enter key support for search
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('userSearchInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchUsers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test message form handler
|
||||
document.getElementById('testMessageForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const chatId = document.getElementById('testChatId').value;
|
||||
const message = document.getElementById('testMessage').value;
|
||||
|
||||
if (!chatId || !message) {
|
||||
showAlert('Please fill all fields', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/telegram/send`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: parseInt(chatId),
|
||||
text: message
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Message sent successfully', 'success');
|
||||
document.getElementById('testMessage').value = '';
|
||||
} else {
|
||||
const error = await response.text();
|
||||
showAlert('Error sending message: ' + error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Error sending message: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Update loadPageData function to include telegram
|
||||
const originalLoadPageData = window.loadPageData;
|
||||
window.loadPageData = function(page) {
|
||||
if (page === 'telegram') {
|
||||
loadTelegram();
|
||||
} else if (originalLoadPageData) {
|
||||
originalLoadPageData(page);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
loadPageData('dashboard');
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user