diff --git a/Cargo.lock b/Cargo.lock index 16b58ae..e8f2b44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,22 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "agirs" -version = "0.1.0" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" @@ -30,10 +26,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "amirs" -version = "0.1.0" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -50,10 +42,19 @@ dependencies = [ ] [[package]] -name = "arirs" +name = "asterisk-rs-agi" +version = "0.1.0" + +[[package]] +name = "asterisk-rs-ami" +version = "0.1.0" + +[[package]] +name = "asterisk-rs-ari" version = "0.1.0" dependencies = [ "chrono", + "derive-getters", "derive_more", "futures-channel", "futures-util", @@ -61,6 +62,8 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_qs", + "strum", "thiserror", "tokio", "tokio-tungstenite", @@ -83,17 +86,17 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets", ] [[package]] @@ -137,9 +140,9 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.15" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "shlex", ] @@ -183,9 +186,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -206,6 +209,17 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "derive-getters" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ef43543e701c01ad77d3a5922755c6a1d71b22d942cb8042be4994b380caff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -224,7 +238,6 @@ dependencies = [ "proc-macro2", "quote", "syn", - "unicode-xid", ] [[package]] @@ -374,9 +387,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "h2" @@ -403,6 +416,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -471,9 +490,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http", @@ -484,6 +503,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -504,9 +524,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", @@ -557,9 +577,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -567,9 +587,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "itoa" @@ -604,16 +624,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.22" @@ -643,11 +653,11 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -700,18 +710,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" [[package]] name = "openssl" @@ -763,29 +773,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -848,6 +835,54 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -887,15 +922,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.10.6" @@ -967,7 +993,10 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -975,11 +1004,13 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "windows-registry", ] @@ -1004,11 +1035,17 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustix" -version = "0.38.35" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags", "errno", @@ -1019,11 +1056,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1048,15 +1086,21 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -1065,19 +1109,13 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "security-framework" version = "2.11.1" @@ -1133,6 +1171,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1171,15 +1220,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "slab" version = "0.4.9" @@ -1211,6 +1251,28 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1219,9 +1281,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.76" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -1326,9 +1388,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -1380,9 +1440,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -1500,6 +1560,7 @@ dependencies = [ "rand", "sha1", "thiserror", + "url", "utf-8", ] @@ -1517,9 +1578,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" @@ -1530,12 +1591,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-xid" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" - [[package]] name = "untrusted" version = "0.9.0" @@ -1669,6 +1724,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 2eb8ac0..b316bbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,40 @@ [workspace] -members = ["agirs", "amirs", "arirs"] +members = ["crates/*"] resolver = "2" + +[workspace.package] +edition = "2021" +readme = "README.md" +version = "0.1.0" + +[workspace.lints.rust] +# TEMP: +# missing_docs = "warn" +unused_must_use = "deny" + +[workspace.lints.clippy] +self_named_module_files = "deny" + +[profile.dev] +# https://davidlattimore.github.io/posts/2024/02/04/speeding-up-the-rust-edit-build-run-cycle.html#avoid-linking-debug-info +debug = 0 +strip = "debuginfo" + +[workspace.dependencies] +chrono = "0.4" +derive-getters = "0.5" +derive_more = "1.0" +futures-channel = "0.3" +futures-util = "0.3" +rand = "0.8" +reqwest = { version = "0.12", default-features = true, features = ["charset", "http2", "rustls-tls"] } +serde = "1.0" +serde_json = "1.0" +serde_qs = "0.13" +strum = "0.26" +thiserror = "1.0" +tokio = "1.40" +tokio-tungstenite = "0.23" +tracing = "0.1" +tracing-subscriber = "0.3" +url = "2.5" diff --git a/README.md b/README.md index 00a76e2..2525393 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ An Asterisk Library in Rust -## arirs +## `asterisk-rs-ari` A crate for Asterisk REST Interface tooling -## amirs +## `asterisk-rs-ami` A crate for Asterisk Management Interface tooling -## agirs +## `asterisk-rs-agi` A crate for Asterisk Gateway Interface tooling diff --git a/agirs/Cargo.toml b/agirs/Cargo.toml deleted file mode 100644 index 9264cb7..0000000 --- a/agirs/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -edition = "2021" -name = "agirs" -version = "0.1.0" - -[dependencies] diff --git a/amirs/Cargo.toml b/amirs/Cargo.toml deleted file mode 100644 index d489c4c..0000000 --- a/amirs/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -edition = "2021" -name = "amirs" -version = "0.1.0" - -[dependencies] diff --git a/arirs/Cargo.toml b/arirs/Cargo.toml deleted file mode 100644 index d851e06..0000000 --- a/arirs/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -edition = "2021" -name = "arirs" -version = "0.1.0" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -chrono = { version = "0.4.38", features = ["serde"] } -derive_more = { version = "1.0.0", features = ["display"] } -futures-channel = "0.3.30" -futures-util = "0.3.30" -rand = "0.8.5" -reqwest = { version = "0.12.7", features = ["json"] } -serde = { version = "1.0.210", features = ["derive"] } -serde_json = "1.0.128" -thiserror = "1.0.63" -tokio = { version = "1.40.0", features = ["full"] } -tokio-tungstenite = "0.23.1" -tracing = { version = "0.1.40", features = ["attributes"] } -url = "2.5.2" - -[dev-dependencies] -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } diff --git a/arirs/examples/dtmf.rs b/arirs/examples/dtmf.rs deleted file mode 100644 index 33e197e..0000000 --- a/arirs/examples/dtmf.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use arirs::{client::Client, Event}; -use tracing::{debug, error}; -use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::registry() - .with(fmt::layer()) - .with(EnvFilter::from_default_env()) - .init(); - - let (tx, mut rx) = tokio::sync::mpsc::channel(1024); - let dtmf_buffer = Arc::new(Mutex::new(String::new())); - - let client = Client::new() - .url(url::Url::parse("http://localhost:8088/ari/")?) - .username("asterisk") - .password("asterisk") - .app_name("ari") - .handler(tx) - .build()?; - - tokio::spawn(async move { - if let Err(e) = client.run().await { - error!("Error: {}", e); - } - }); - - while let Some(event) = rx.recv().await { - match event { - Event::ChannelDtmfReceived(event) => { - debug!("Received DTMF: {}", event.digit); - dtmf_buffer.lock().unwrap().push_str(&event.digit); - } - Event::StasisEnd(_) => { - debug!("Stasis ended, DTMF buffer: {}", dtmf_buffer.lock().unwrap()); - dtmf_buffer.lock().unwrap().clear(); - } - _ => {} - } - } - - Ok(()) -} diff --git a/arirs/examples/list-channels.rs b/arirs/examples/list-channels.rs deleted file mode 100644 index 0a67fdd..0000000 --- a/arirs/examples/list-channels.rs +++ /dev/null @@ -1,24 +0,0 @@ -use arirs::{channel::Channel, client::Client, Result}; -use tracing::debug; -use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::registry() - .with(fmt::layer()) - .with(EnvFilter::from_default_env()) - .init(); - - let client = Client::new() - .url(url::Url::parse("http://localhost:8088/ari")?) - .username("asterisk") - .password("asterisk") - .app_name("ari") - .build()?; - - for channel in Channel::list(&client).await? { - debug!("Channel ID: {}", channel.id); - } - - Ok(()) -} diff --git a/arirs/examples/originate.rs b/arirs/examples/originate.rs deleted file mode 100644 index b8cb799..0000000 --- a/arirs/examples/originate.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::collections::HashMap; - -use arirs::{ - channel::{Channel, OriginateParams}, - client::Client, - Result, -}; -use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; - -const APP_NAME: &str = "ari"; - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::registry() - .with(fmt::layer()) - .with(EnvFilter::from_default_env()) - .init(); - - let client = Client::new() - .url(url::Url::parse("http://localhost:8088/ari")?) - .username("asterisk") - .password("asterisk") - .app_name(APP_NAME) - .build()?; - - Channel::originate( - &client, - "PJSIP/1000", - OriginateParams::Application { - app: APP_NAME, - app_args: vec![], - }, - None, - None, - None, - None, - None, - vec!["alaw,ulaw"], - HashMap::new(), - ) - .await?; - - Ok(()) -} diff --git a/arirs/examples/playback.rs b/arirs/examples/playback.rs deleted file mode 100644 index a13d9d1..0000000 --- a/arirs/examples/playback.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::sync::Arc; - -use arirs::{client::Client, Event}; -use tracing::{error, level_filters::LevelFilter}; -use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; - -const APP_NAME: &str = "ari"; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::registry().with(fmt::layer()).with(LevelFilter::TRACE).init(); - - let (tx, mut rx) = tokio::sync::mpsc::channel(1024); - - let client = Arc::new( - Client::new() - .url(url::Url::parse("http://localhost:8088/ari")?) - .username("asterisk") - .password("asterisk") - .app_name(APP_NAME) - .handler(tx) - .build()?, - ); - - let client_clone = client.clone(); - tokio::spawn(async move { - if let Err(e) = client_clone.run().await { - error!("Error: {}", e); - } - }); - - while let Some(event) = rx.recv().await { - match event { - Event::StasisStart(e) => { - let channel = e.channel; - channel.play_media(&client, "sound:hello", Some("en"), None, None, None).await?; - } - _ => {} - } - } - - Ok(()) -} diff --git a/arirs/src/bridge.rs b/arirs/src/bridge.rs deleted file mode 100644 index eee2d90..0000000 --- a/arirs/src/bridge.rs +++ /dev/null @@ -1,81 +0,0 @@ -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::{client::Client, playback::Playback, recording::LiveRecording, Result}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Bridge { - pub id: String, -} - -impl Bridge { - #[instrument(level = "debug")] - pub async fn destroy(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn add_channel(&self, _client: &Client, _channel_id: &str) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn remove_channel(&self, _client: &Client, _channel_id: &str) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn set_channel_as_video_source(&self, _client: &Client, _channel_id: &str, _video_source_id: &str) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn unset_video_source(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn start_moh(&self, _client: &Client, _moh_class: &str) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn stop_moh(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn play_media(&self, _client: &Client, _playback: &Playback) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn stop_media(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn start_recording(&self, _client: &Client, _recording: &LiveRecording) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn list_bridges(_client: &Client) -> Result> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn create_bridge(_client: &Client, _bridge_id: &str) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn create_bridge_with_id(_client: &Client, _bridge_id: &str, _bridge: &Bridge) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn get_bridge(_client: &Client, _bridge_id: &str) -> Result { - unimplemented!() - } -} diff --git a/arirs/src/channel.rs b/arirs/src/channel.rs deleted file mode 100644 index 0e430a1..0000000 --- a/arirs/src/channel.rs +++ /dev/null @@ -1,855 +0,0 @@ -use std::collections::HashMap; - -use chrono::{DateTime, Duration}; -use derive_more::Display; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use tracing::{event, instrument, Level}; -use url::Url; - -use crate::{client::Client, playback::Playback, recording::LiveRecording, rtp_statistics::RtpStatistics, variable::Variable, Result}; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct Channel { - pub id: String, - pub name: String, - pub state: String, - pub protocol_id: String, - pub caller: Caller, - pub connected: Caller, - pub accountcode: String, - pub dialplan: Dialplan, - pub creationtime: String, - pub language: String, -} - -#[derive(Debug)] -pub enum OriginateParams<'a> { - Extension { - extension: &'a str, - context: Option<&'a str>, - priority: Option, - label: Option<&'a str>, - }, - Application { - app: &'a str, - app_args: Vec<&'a str>, - }, -} - -#[derive(Debug, Display)] -pub enum Reason { - #[display("{}", _0)] - Code(u16), - #[display("normal")] - Normal, - #[display("busy")] - Busy, - #[display("congestion")] - Congestion, - #[display("no_answer")] - NoAnswer, - #[display("timeout")] - Timeout, - #[display("rejected")] - Rejected, - #[display("unallocated")] - Unallocated, - #[display("normal_unspecified")] - NormalUnspecified, - #[display("number_incomplete")] - NumberIncomplete, - #[display("codec_mismatch")] - CodecMismatch, - #[display("interworking")] - Interworking, - #[display("failure")] - Failure, - #[display("answered_elsewhere")] - AnsweredElsewhere, -} - -#[derive(Debug, Display)] -pub enum Direction { - #[display("in")] - In, - #[display("out")] - Out, - #[display("both")] - Both, -} - -#[derive(Debug, Display)] -pub enum RecordingAction { - #[display("overwrite")] - Overwrite, - #[display("append")] - Append, - #[display("fail")] - Fail, -} - -#[derive(Debug, Display)] -pub enum RecordingTermination { - #[display("none")] - None, - #[display("any")] - Any, - #[display("*")] - Asterisk, - #[display("#")] - Octothorpe, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct StasisStart { - pub timestamp: DateTime, - pub args: Vec, - pub channel: Channel, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct StasisEnd { - pub timestamp: DateTime, - pub channel: Channel, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct ChannelCreated { - pub timestamp: DateTime, - pub channel: Option, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct ChannelDestroyed { - pub timestamp: DateTime, - pub cause: i32, - pub cause_txt: String, - pub channel: Channel, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct ChannelVarset { - pub timestamp: DateTime, - pub variable: String, - pub value: String, - pub channel: Option, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct ChannelHangupRequest { - pub timestamp: DateTime, - pub soft: Option, - pub cause: i32, - pub channel: Channel, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct ChannelDialplan { - pub timestamp: DateTime, - pub dialplan_app: String, - pub dialplan_app_data: String, - pub channel: Channel, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct ChannelStateChange { - pub timestamp: DateTime, - pub channel: Channel, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct ChannelDtmfReceived { - pub timestamp: DateTime, - pub digit: String, - pub duration_ms: i32, - pub channel: Channel, - pub asterisk_id: String, - pub application: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct Caller { - pub name: String, - pub number: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct Dialplan { - pub context: String, - pub exten: String, - pub priority: i32, - pub app_name: String, - pub app_data: String, -} - -impl Channel { - #[instrument(level = "debug")] - pub async fn hangup(self, client: &Client, reason: Reason) -> Result<()> { - let mut url = client.url.join(&format!("channels/{}", self.id))?; - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - - match reason { - Reason::Code(_) => url.append_pair("reason_code", &format!("{}", reason)), - _ => url.append_pair("reason", &format!("{}", reason)), - }; - - reqwest::Client::new().delete(url.finish().to_owned()).send().await?; - - event!(Level::INFO, "hung up channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub fn continue_in_dialplan(self, _client: &Client) -> Result<()> { - unimplemented!() - } - - /// Transfer the channel to another ARI application. - /// Same as `move` in Asterisk - #[instrument(level = "debug")] - pub fn transfer(self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn answer(&self, client: &Client) -> Result<()> { - let url = client - .url - .join(&format!("channels/{}/answer", self.id))? - .query_pairs_mut() - .append_pair("api_key", &client.get_api_key()) - .finish() - .to_owned(); - - reqwest::Client::new().post(url).send().await?; - event!(Level::INFO, "answered channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub async fn start_ringing(&self, client: &Client) -> Result<()> { - let url = client - .url - .join(&format!("channels/{}/ring", self.id))? - .query_pairs_mut() - .append_pair("api_key", &client.get_api_key()) - .finish() - .to_owned(); - - reqwest::Client::new().post(url).send().await?; - event!(Level::INFO, "started ringing channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub async fn stop_ringing(&self, client: &Client) -> Result<()> { - let url = client - .url - .join(&format!("channels/{}/ring", self.id))? - .query_pairs_mut() - .append_pair("api_key", &format!("{}:{}", client.username, client.password)) - .finish() - .to_owned(); - - reqwest::Client::new().delete(url).send().await?; - event!(Level::INFO, "stopped ringing channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub async fn send_dtmf( - &self, - client: &Client, - dtmf: &str, - before: Option, - between: Option, - duration: Option, - after: Option, - ) -> Result<()> { - let mut url = client.url.join(&format!("channels/{}/dtmf", self.id))?; - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - - url.append_pair("dtmf", dtmf) - .append_pair("between", &between.map(|d| d.num_milliseconds()).unwrap_or(100).to_string()) - .append_pair("duration", &duration.map(|d| d.num_milliseconds()).unwrap_or(100).to_string()); - - if let Some(before) = before { - url.append_pair("before", &before.num_milliseconds().to_string()); - } - - if let Some(after) = after { - url.append_pair("after", &after.num_milliseconds().to_string()); - } - - reqwest::Client::new().post(url.finish().to_owned()).send().await?; - - event!(Level::INFO, "sent dtmf '{}' to channel with id {}", dtmf, self.id); - - Ok(()) - } - - #[instrument(level = "debug")] - pub async fn mute(&self, client: &Client, direction: Direction) -> Result<()> { - let url = client - .url - .join(&format!("channels/{}/mute", self.id))? - .query_pairs_mut() - .append_pair("api_key", &client.get_api_key()) - .append_pair("direction", &format!("{}", direction)) - .finish() - .to_owned(); - - reqwest::Client::new().post(url).send().await?; - event!(Level::INFO, "muted channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub async fn unmute(&self, client: &Client, direction: Direction) -> Result<()> { - let url = client - .url - .join(&format!("channels/{}/mute", self.id))? - .query_pairs_mut() - .append_pair("api_key", &client.get_api_key()) - .append_pair("direction", &format!("{}", direction)) - .finish() - .to_owned(); - - reqwest::Client::new().delete(url).send().await?; - event!(Level::INFO, "unmuted channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub async fn hold(&self, client: &Client) -> Result<()> { - let url = client - .url - .join(&format!("channels/{}/hold", self.id))? - .query_pairs_mut() - .append_pair("api_key", &client.get_api_key()) - .finish() - .to_owned(); - - reqwest::Client::new().post(url).send().await?; - event!(Level::INFO, "started hold on channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub async fn unhold(&self, client: &Client) -> Result<()> { - let url = client - .url - .join(&format!("channels/{}/hold", self.id))? - .query_pairs_mut() - .append_pair("api_key", &client.get_api_key()) - .finish() - .to_owned(); - - reqwest::Client::new().delete(url).send().await?; - event!(Level::INFO, "stopped hold on channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub fn start_moh(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub fn stop_moh(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub fn start_silence(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub fn stop_silence(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn play_media( - &self, - client: &Client, - media: &str, - lang: Option<&str>, - offset_ms: Option, - skip_ms: Option, - playback_id: Option<&str>, - ) -> Result { - let mut url = client.url.join(&format!("channels/{}/play", self.id))?; - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - url.append_pair("media", media); - - if let Some(lang) = lang { - url.append_pair("lang", lang); - } - - if let Some(offset_ms) = offset_ms { - url.append_pair("offset_ms", &offset_ms.to_string()); - } - - if let Some(skip_ms) = skip_ms { - url.append_pair("skip_ms", &skip_ms.to_string()); - } - - if let Some(playback_id) = playback_id { - url.append_pair("playback_id", playback_id); - } - - let playback = reqwest::Client::new() - .post(url.finish().to_owned()) - .send() - .await? - .json::() - .await?; - - event!( - Level::INFO, - "started media playback with id {} on channel with id {}", - playback.id, - self.id - ); - - Ok(playback) - } - - #[instrument(level = "debug")] - pub async fn play_media_with_id( - &self, - client: &Client, - playback_id: &str, - media: Vec<&str>, - lang: Option<&str>, - offset_ms: Option, - skip_ms: Option, - ) -> Result { - let mut url = client.url.join(&format!("channels/{}/play/{}/media", self.id, playback_id))?; - - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - let media = media.join(","); - url.append_pair("media", &media); - - if let Some(lang) = lang { - url.append_pair("lang", lang); - } - - if let Some(offset_ms) = offset_ms { - url.append_pair("offset_ms", &offset_ms.to_string()); - } - - if let Some(skip_ms) = skip_ms { - url.append_pair("skip_ms", &skip_ms.to_string()); - } - - let playback = reqwest::Client::new() - .post(url.finish().to_owned()) - .send() - .await? - .json::() - .await?; - - event!( - Level::INFO, - "started media playback with id {} on channel with id {}", - playback.id, - self.id - ); - - Ok(playback) - } - - #[allow(clippy::too_many_arguments)] - #[instrument(level = "debug")] - pub async fn record( - &self, - client: &Client, - name: &str, - format: &str, - max_duration_seconds: Option, - max_silence_seconds: Option, - if_exists: RecordingAction, - beep: bool, - terminate_on: RecordingTermination, - ) -> Result { - let mut url = client.url.join(&format!("channels/{}/record", self.id))?; - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - - url.append_pair("name", name) - .append_pair("format", format) - .append_pair("if_exists", &format!("{}", if_exists)) - .append_pair("beep", &beep.to_string()) - .append_pair("terminate_on", &format!("{}", terminate_on)); - - if let Some(max_duration_seconds) = max_duration_seconds { - url.append_pair("max_duration_seconds", &max_duration_seconds.to_string()); - } - - if let Some(max_silence_seconds) = max_silence_seconds { - url.append_pair("max_silence_seconds", &max_silence_seconds.to_string()); - } - - let recording = reqwest::Client::new() - .post(url.finish().to_owned()) - .send() - .await? - .json::() - .await?; - - event!( - Level::INFO, - "started recording with id {} on channel with id {}", - recording.id, - self.id - ); - - Ok(recording) - } - - #[instrument(level = "debug")] - pub fn get_variable(&self, _client: &Client) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub fn set_variable(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn dial(&self, client: &Client, caller_id: Option<&str>, timeout: Option) -> Result<()> { - let mut url = client.url.join(&format!("channels/{}/dial", self.id))?; - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - - if let Some(caller_id) = caller_id { - url.append_pair("callerId", caller_id); - } - - if let Some(timeout) = timeout { - url.append_pair("timeout", &timeout.to_string()); - } - - reqwest::Client::new().post(url.finish().to_owned()).send().await?; - - event!(Level::INFO, "dialed channel with id {}", self.id); - Ok(()) - } - - #[instrument(level = "debug")] - pub fn get_rtp_statistics(&self, _client: &Client) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn list(client: &Client) -> Result> { - let url: Url = client - .url - .join("channels")? - .query_pairs_mut() - .append_pair("api_key", &client.get_api_key()) - .finish() - .to_owned(); - - let channels = reqwest::get(url).await?.json::>().await?; - event!(Level::INFO, "received channels"); - Ok(channels) - } - - #[allow(clippy::too_many_arguments)] - #[instrument(level = "debug")] - pub async fn create( - client: &Client, - endpoint: &str, - app: &str, - app_args: Vec<&str>, - channel_id: Option<&str>, - other_channel_id: Option<&str>, - originator: Option<&str>, - formats: Vec<&str>, - variables: HashMap<&str, &str>, - ) -> Result { - let mut url = client.url.join("channels")?; - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - url.append_pair("endpoint", endpoint).append_pair("app", app); - - if !formats.is_empty() { - let formats = formats.join(","); - url.append_pair("formats", &formats); - } - - if !app_args.is_empty() { - let app_args = app_args.join(","); - url.append_pair("app_args", &app_args); - } - - if let Some(channel_id) = channel_id { - url.append_pair("channel_id", channel_id); - } - - if let Some(other_channel_id) = other_channel_id { - url.append_pair("other_channel_id", other_channel_id); - } - - if let Some(originator) = originator { - url.append_pair("originator", originator); - } - - let body = json!({ - "variables": variables - }); - - let channel = reqwest::Client::new() - .post(url.finish().to_owned()) - .json(&body) - .send() - .await? - .json::() - .await?; - - event!(Level::INFO, "created channel with id {}", channel.id); - Ok(channel) - } - - #[instrument(level = "debug")] - pub async fn get(client: &Client, channel_id: &str) -> Result { - let url = client - .url - .join(&format!("channels/{}", channel_id))? - .query_pairs_mut() - .append_pair("api_key", &client.get_api_key()) - .finish() - .to_owned(); - - let channel = reqwest::get(url).await?.json::().await?; - event!(Level::INFO, "received channel with id {}", channel.id); - Ok(channel) - } - - #[allow(clippy::too_many_arguments)] - #[instrument(level = "debug")] - pub async fn originate<'a>( - client: &Client, - endpoint: &str, - params: OriginateParams<'a>, - caller_id: Option<&str>, - timeout: Option, - channel_id: Option<&str>, - other_channel_id: Option<&str>, - originator: Option<&str>, - formats: Vec<&str>, - variables: HashMap<&str, &str>, - ) -> Result { - let mut url = client.url.join("channels")?; - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - - url.append_pair("endpoint", endpoint) - .append_pair("timeout", &timeout.unwrap_or(30).to_string()); - - if !formats.is_empty() { - let formats = formats.join(","); - url.append_pair("formats", &formats); - } - - match params { - OriginateParams::Extension { - extension, - context, - priority, - label, - } => { - url.append_pair("extension", extension); - - if let Some(context) = context { - url.append_pair("context", context); - } - - if let Some(priority) = priority { - url.append_pair("priority", &priority.to_string()); - } - - if let Some(label) = label { - url.append_pair("label", label); - } - } - OriginateParams::Application { app, app_args } => { - url.append_pair("app", app); - - if !app_args.is_empty() { - let app_args = app_args.join(","); - url.append_pair("app_args", &app_args); - } - } - } - - if let Some(caller_id) = caller_id { - url.append_pair("callerId", caller_id); - } - - if let Some(channel_id) = channel_id { - url.append_pair("channel_id", channel_id); - } - - if let Some(other_channel_id) = other_channel_id { - url.append_pair("other_channel_id", other_channel_id); - } - - if let Some(originator) = originator { - url.append_pair("originator", originator); - } - - let body = json!({ - "variables": variables - }); - - let channel = reqwest::Client::new() - .post(url.finish().to_owned()) - .json(&body) - .send() - .await? - .json::() - .await?; - - event!(Level::INFO, "originated channel with id {}", channel.id); - Ok(channel) - } - - #[allow(clippy::too_many_arguments)] - #[instrument(level = "debug")] - pub async fn originate_with_id<'a>( - client: &Client, - channel_id: &str, - endpoint: &str, - params: OriginateParams<'a>, - caller_id: Option<&str>, - timeout: Option, - other_channel_id: Option<&str>, - originator: Option<&str>, - formats: Vec<&str>, - variables: HashMap<&str, &str>, - ) -> Result { - let mut url = client.url.join(&format!("channels/{}", channel_id))?; - let mut url = url.query_pairs_mut(); - client.add_api_key(&mut url); - - url.append_pair("endpoint", endpoint) - .append_pair("timeout", &timeout.unwrap_or(30).to_string()); - - if !formats.is_empty() { - let formats = formats.join(","); - url.append_pair("formats", &formats); - } - - match params { - OriginateParams::Extension { - extension, - context, - priority, - label, - } => { - url.append_pair("extension", extension); - - if let Some(context) = context { - url.append_pair("context", context); - } - - if let Some(priority) = priority { - url.append_pair("priority", &priority.to_string()); - } - - if let Some(label) = label { - url.append_pair("label", label); - } - } - OriginateParams::Application { app, app_args } => { - url.append_pair("app", app); - - if !app_args.is_empty() { - let app_args = app_args.join(","); - url.append_pair("app_args", &app_args); - } - } - } - - if let Some(caller_id) = caller_id { - url.append_pair("callerId", caller_id); - } - - if let Some(other_channel_id) = other_channel_id { - url.append_pair("otherChannelId", other_channel_id); - } - - if let Some(originator) = originator { - url.append_pair("originator", originator); - } - - let body = json!({ - "variables": variables - }); - - let channel = reqwest::Client::new() - .post(url.finish().to_owned()) - .json(&body) - .send() - .await? - .json::() - .await?; - - event!(Level::INFO, "originated channel with id {}", channel.id); - Ok(channel) - } - - #[instrument(level = "debug")] - pub fn snoop(&self, _channel_id: &str) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub fn snoop_with_id(&self, _channel_id: &str) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub fn start_external_media(&self, _channel_id: &str) -> Result { - unimplemented!() - } -} diff --git a/arirs/src/client.rs b/arirs/src/client.rs deleted file mode 100644 index 6ad5bd2..0000000 --- a/arirs/src/client.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::time::Duration; - -use futures_util::{SinkExt, StreamExt}; -use rand::Rng; -use tokio::{sync::mpsc::Sender, time::interval}; -use tokio_tungstenite::{connect_async, tungstenite}; -use tracing::{event, instrument, Level}; -use url::Url; - -use crate::{Event, Result}; - -impl ClientBuilder { - pub fn url(mut self, url: Url) -> Self { - self.0.url = url; - self - } - - pub fn username(mut self, username: &str) -> Self { - self.0.username = username.to_string(); - self - } - - pub fn password(mut self, password: &str) -> Self { - self.0.password = password.to_string(); - self - } - - pub fn app_name(mut self, app_name: &str) -> Self { - self.0.app_name = app_name.to_string(); - self - } - - pub fn handler(mut self, tx: Sender) -> Self { - self.0.ws_channel = Some(tx); - self - } - - #[instrument(level = "debug")] - pub fn build(self) -> Result { - let mut ws_url = self.0.url.join("events")?; - - let scheme = match ws_url.scheme() { - "http" => "ws", - "https" => "wss", - _ => { - event!(Level::ERROR, "Unsupported scheme '{}'", ws_url.scheme()); - return Err(tungstenite::error::UrlError::UnsupportedUrlScheme.into()); - } - }; - - if ws_url.set_scheme(scheme).is_err() { - return Err(tungstenite::error::UrlError::UnsupportedUrlScheme.into()); - } - - ws_url - .query_pairs_mut() - .append_pair("app", &self.0.app_name) - .append_pair("api_key", &format!("{}:{}", self.0.username, self.0.password)) - .append_pair("subscribeAll", "true"); - - event!(Level::TRACE, "Using REST API server with URL '{}'", self.0.url); - - event!(Level::TRACE, "Using WebSocket server with URL '{}'", ws_url); - - Ok(Client { - url: self.0.url, - ws_url, - username: self.0.username, - password: self.0.password, - app_name: self.0.app_name, - ws_channel: self.0.ws_channel, - }) - } -} - -#[derive(Debug, Default)] -pub struct ClientBuilder(Client); - -impl Client { - #[allow(clippy::new_ret_no_self)] - pub fn new() -> ClientBuilder { - ClientBuilder(Client::default()) - } - - #[instrument(level = "debug")] - pub fn handle_message(&self, message: Vec) { - let data = String::from_utf8(message).unwrap(); - - event!(Level::TRACE, "Parsing event"); - - let event: Event = match serde_json::from_str(&data) { - Ok(data) => data, - Err(e) => { - event!(Level::ERROR, "Error: {}", e); - event!(Level::ERROR, "Data: {}", data); - return; - } - }; - - event!(Level::TRACE, "Event parsed successfully"); - - if let Some(tx) = &self.ws_channel { - event!(Level::INFO, "Sending event to channel"); - if let Err(e) = tx.try_send(event) { - event!(Level::ERROR, "Error sending event: {}", e); - } - } - } - - #[instrument(level = "debug")] - pub async fn run(&self) -> Result<()> { - event!(Level::INFO, "Connecting to Asterisk"); - - let (ws_stream, _) = match connect_async(&self.ws_url.to_string()).await { - Ok(stream) => stream, - Err(e) => { - event!(Level::ERROR, "Failed to connect to Asterisk: {}", e); - return Err(e.into()); - } - }; - - event!(Level::INFO, "WebSocket handshake has been successfully completed"); - - let (mut ws_sender, mut ws_receiver) = ws_stream.split(); - let mut interval = interval(Duration::from_millis(5000)); - - loop { - tokio::select! { - message = ws_receiver.next() => { - match message { - Some(message) => { - let message = message?; - match message { - tungstenite::Message::Text(_) => { - event!(Level::INFO, "Received WebSocket Text"); - self.handle_message(message.into_data()); - } - tungstenite::Message::Ping(data) => { - event!(Level::INFO, "Received WebSocket Ping, sending Pong"); - ws_sender.send(tungstenite::Message::Pong(data)).await?; - } - tungstenite::Message::Pong(_) => { - event!(Level::INFO, "Received WebSocket Pong"); - }, - tungstenite::Message::Close(frame) => { - event!(Level::INFO, "WebSocket closed: {:?}", frame); - break; - }, - _ => { - event!(Level::INFO, "Unknown WebSocket message"); - } - } - } - None => { - event!(Level::INFO, "WebSocket closed"); - break; - } - } - } - _ = interval.tick() => { - // every 5 seconds we are sending ping to keep connection alive - // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html - let random_bytes = rand::thread_rng().gen::<[u8; 32]>().to_vec(); - let _ = ws_sender.send(tungstenite::Message::Ping(random_bytes)).await; - event!(Level::DEBUG, "ARI connection ping sent"); - } - } - } - - Ok(()) - } - - pub(crate) fn get_api_key(&self) -> String { - format!("{}:{}", self.username, self.password) - } - - pub(crate) fn add_api_key(&self, url: &mut url::form_urlencoded::Serializer) { - url.append_pair("api_key", &self.get_api_key()); - } -} - -impl Default for Client { - fn default() -> Self { - Self { - url: match Url::parse("http://localhost:8088/") { - Ok(url) => url, - Err(_) => panic!("Failed to parse URL"), - }, - ws_url: match Url::parse("ws://localhost:8088/ari/events?app=ari&api_key=asterisk:asterisk&subscribeAll=true") { - Ok(url) => url, - Err(_) => panic!("Failed to parse URL"), - }, - username: "asterisk".to_string(), - password: "asterisk".to_string(), - app_name: "ari".to_string(), - ws_channel: None, - } - } -} - -#[derive(Debug)] -pub struct Client { - pub url: Url, - pub ws_url: Url, - pub username: String, - pub password: String, - pub app_name: String, - pub ws_channel: Option>, -} diff --git a/arirs/src/device.rs b/arirs/src/device.rs deleted file mode 100644 index 2ddfc8b..0000000 --- a/arirs/src/device.rs +++ /dev/null @@ -1,18 +0,0 @@ -use chrono::DateTime; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct DeviceStateChanged { - pub application: String, - pub timestamp: DateTime, - pub device_state: DeviceState, - pub asterisk_id: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct DeviceState { - pub name: String, - pub state: String, -} diff --git a/arirs/src/lib.rs b/arirs/src/lib.rs deleted file mode 100644 index 939858a..0000000 --- a/arirs/src/lib.rs +++ /dev/null @@ -1,57 +0,0 @@ -use channel::{ - ChannelCreated, ChannelDestroyed, ChannelDialplan, ChannelDtmfReceived, ChannelHangupRequest, ChannelStateChange, ChannelVarset, - StasisEnd, StasisStart, -}; -use device::DeviceStateChanged; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use tokio::task::JoinError; -use tokio_tungstenite::tungstenite; - -pub mod bridge; -pub mod channel; -pub mod client; -pub mod device; -pub mod playback; -pub mod recording; -pub mod rtp_statistics; -pub mod variable; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum AriError { - #[error("URL parsing error")] - UrlParseError(#[from] url::ParseError), - #[error("WebSocket error")] - TungsteniteError(#[from] tungstenite::Error), - #[error("HTTP Request error")] - ReqwestError(#[from] reqwest::Error), - #[error("Join Error")] - JoinError(#[from] JoinError), - #[error("Unknown error occurred: {0}")] - Unknown(String), -} - -impl From for AriError { - fn from(err: tungstenite::error::UrlError) -> Self { - AriError::TungsteniteError(err.into()) - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type")] -pub enum Event { - StasisStart(StasisStart), - StasisEnd(StasisEnd), - ChannelCreated(ChannelCreated), - ChannelDestroyed(ChannelDestroyed), - ChannelVarset(ChannelVarset), - ChannelHangupRequest(ChannelHangupRequest), - ChannelDialplan(ChannelDialplan), - ChannelStateChange(ChannelStateChange), - ChannelDtmfReceived(ChannelDtmfReceived), - DeviceStateChanged(DeviceStateChanged), - #[serde(other)] - Unknown, -} diff --git a/arirs/src/playback.rs b/arirs/src/playback.rs deleted file mode 100644 index 9c7e30a..0000000 --- a/arirs/src/playback.rs +++ /dev/null @@ -1,43 +0,0 @@ -use derive_more::Display; -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::{client::Client, Result}; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct Playback { - pub id: String, -} - -impl Playback { - #[instrument(level = "debug")] - pub async fn get_playback(_client: &Client, _playback_id: &str) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn control(&self, _client: &Client, _operation: Operation) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn stop(&self, _client: &Client) -> Result<()> { - unimplemented!() - } -} - -#[derive(Serialize, Deserialize, Debug, Display)] -#[serde(rename_all = "snake_case")] -pub enum Operation { - #[display("restart")] - Restart, - #[display("pause")] - Pause, - #[display("unpause")] - Unpause, - #[display("reverse")] - Reverse, - #[display("forward")] - Forward, -} diff --git a/arirs/src/recording.rs b/arirs/src/recording.rs deleted file mode 100644 index a00f67a..0000000 --- a/arirs/src/recording.rs +++ /dev/null @@ -1,78 +0,0 @@ -use serde::{Deserialize, Serialize}; -use tracing::instrument; - -use crate::{client::Client, Result}; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct LiveRecording { - pub id: String, - pub name: String, -} - -impl LiveRecording { - #[instrument(level = "debug")] - pub async fn get(_recording_name: &str) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn discard(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - // TODO: explore if it's possible to return a StoredRecording - #[instrument(level = "debug")] - pub async fn stop(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn pause(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn resume(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn mute(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn unmute(&self, _client: &Client) -> Result<()> { - unimplemented!() - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct StoredRecording { - pub id: String, - pub format: String, -} - -impl StoredRecording { - #[instrument(level = "debug")] - pub async fn list(_client: &Client) -> Result> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn get(_recording_name: &str) -> Result { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn delete(&self, _client: &Client) -> Result<()> { - unimplemented!() - } - - #[instrument(level = "debug")] - pub async fn download(&self, _client: &Client) -> Result<&[u8]> { - unimplemented!() - } -} diff --git a/arirs/src/rtp_statistics.rs b/arirs/src/rtp_statistics.rs deleted file mode 100644 index ee1b32d..0000000 --- a/arirs/src/rtp_statistics.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct RtpStatistics { - pub id: String, -} diff --git a/arirs/src/variable.rs b/arirs/src/variable.rs deleted file mode 100644 index e7be11c..0000000 --- a/arirs/src/variable.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "snake_case")] -pub struct Variable { - pub id: String, -} diff --git a/crates/agi/Cargo.toml b/crates/agi/Cargo.toml new file mode 100644 index 0000000..4e20314 --- /dev/null +++ b/crates/agi/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "asterisk-rs-agi" + +edition.workspace = true +readme.workspace = true +version.workspace = true + +[dependencies] diff --git a/agirs/src/lib.rs b/crates/agi/src/lib.rs similarity index 100% rename from agirs/src/lib.rs rename to crates/agi/src/lib.rs diff --git a/crates/ami/Cargo.toml b/crates/ami/Cargo.toml new file mode 100644 index 0000000..354edab --- /dev/null +++ b/crates/ami/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "asterisk-rs-ami" + +edition.workspace = true +readme.workspace = true +version.workspace = true + +[dependencies] diff --git a/amirs/src/lib.rs b/crates/ami/src/lib.rs similarity index 100% rename from amirs/src/lib.rs rename to crates/ami/src/lib.rs diff --git a/crates/ari/Cargo.toml b/crates/ari/Cargo.toml new file mode 100644 index 0000000..8251911 --- /dev/null +++ b/crates/ari/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "asterisk-rs-ari" + +edition.workspace = true +readme.workspace = true +version.workspace = true + +[dependencies] +chrono = { workspace = true, features = ["serde"] } +derive-getters.workspace = true +derive_more = { workspace = true, features = ["deref"] } +futures-channel.workspace = true +futures-util.workspace = true +rand.workspace = true +reqwest = { workspace = true, features = ["json"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +serde_qs.workspace = true +strum = { workspace = true, features = ["derive"] } +thiserror.workspace = true +tokio = { workspace = true, features = ["macros", "rt"] } +tokio-tungstenite = { workspace = true, features = ["url"] } +url.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread"] } +tracing = { workspace = true, features = ["attributes"] } +tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/crates/ari/examples/dtmf.rs b/crates/ari/examples/dtmf.rs new file mode 100644 index 0000000..1fbe61f --- /dev/null +++ b/crates/ari/examples/dtmf.rs @@ -0,0 +1,33 @@ +use std::sync::{Arc, Mutex}; + +use asterisk_rs_ari::{Asterisk, AsteriskEvent}; +use tracing::debug; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + let dtmf_buffer = Arc::new(Mutex::new(String::new())); + + let (_, mut event_listener) = Asterisk::connect("http://localhost:8088/", "asterisk", "asterisk", "ari").await?; + + while let Some(event) = event_listener.recv().await { + match event { + AsteriskEvent::ChannelDtmfReceived(event) => { + debug!("Received DTMF: {}", event.digit()); + dtmf_buffer.lock().unwrap().push_str(event.digit()); + } + AsteriskEvent::StasisEnd(_) => { + debug!("Stasis ended, DTMF buffer: {}", dtmf_buffer.lock().unwrap()); + dtmf_buffer.lock().unwrap().clear(); + } + _ => {} + } + } + + Ok(()) +} diff --git a/crates/ari/examples/list-channels.rs b/crates/ari/examples/list-channels.rs new file mode 100644 index 0000000..c2da906 --- /dev/null +++ b/crates/ari/examples/list-channels.rs @@ -0,0 +1,19 @@ +use asterisk_rs_ari::{AriClient, AriClientError}; +use tracing::debug; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +#[tokio::main] +async fn main() -> Result<(), AriClientError> { + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + let client = AriClient::default(); + + for channel in client.channel_list().await? { + debug!("Channel ID: {}", channel.id()); + } + + Ok(()) +} diff --git a/crates/ari/examples/originate.rs b/crates/ari/examples/originate.rs new file mode 100644 index 0000000..badf7cc --- /dev/null +++ b/crates/ari/examples/originate.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use asterisk_rs_ari::{AriClient, AriClientError, OriginateChannelParams, OriginateParams}; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +const APP_NAME: &str = "ari"; + +#[tokio::main] +async fn main() -> Result<(), AriClientError> { + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + let client = AriClient::default(); + + let originate_params = OriginateChannelParams { + endpoint: "PJSIP/1000", + params: OriginateParams::Application { + app: APP_NAME, + app_args: &[], + }, + caller_id: None, + timeout: None, + channel_id: None, + other_channel_id: None, + originator: None, + formats: &["alaw,ulaw"], + }; + + client.channel_originate(originate_params, &HashMap::new()).await?; + + Ok(()) +} diff --git a/crates/ari/examples/playback.rs b/crates/ari/examples/playback.rs new file mode 100644 index 0000000..c5eefea --- /dev/null +++ b/crates/ari/examples/playback.rs @@ -0,0 +1,31 @@ +use asterisk_rs_ari::{Asterisk, AsteriskEvent, PlayMediaBaseParams, PlayMediaParams}; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::registry().with(fmt::layer()).with(LevelFilter::TRACE).init(); + + let (request_client, mut event_listener) = Asterisk::connect("http://localhost:8088/", "asterisk", "asterisk", "ari").await?; + + while let Some(event) = event_listener.recv().await { + if let AsteriskEvent::StasisStart(event) = event { + request_client + .channel_play_media( + event.channel().id(), + PlayMediaParams { + playback_id: None, + base_params: PlayMediaBaseParams { + media: &["sound:hello"], + lang: Some("en"), + offset_ms: None, + skip_ms: None, + }, + }, + ) + .await?; + } + } + + Ok(()) +} diff --git a/crates/ari/src/ari_client/bridge/core.rs b/crates/ari/src/ari_client/bridge/core.rs new file mode 100644 index 0000000..3a5426a --- /dev/null +++ b/crates/ari/src/ari_client/bridge/core.rs @@ -0,0 +1,65 @@ +use crate::*; + +impl AriClient { + pub async fn bridge_create(&self, _bridge_id: &str) -> AriClientResult { + unimplemented!() + } + + // SUGGESTION(gibbz00): combine with bidge_create by making ID optional + pub async fn bridge_create_with_id(&self, _bridge_id: &str, _bridge: &Bridge) -> AriClientResult { + unimplemented!() + } + + pub async fn bridge_get(_client: &AriClient, _bridge_id: &str) -> AriClientResult { + unimplemented!() + } + + pub async fn bidge_list(&self) -> AriClientResult> { + unimplemented!() + } + + pub async fn bridge_destroy(&self, _bridge_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_add_channel(&self, _bridge_id: &str, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_remove_channel(&self, _bridge_id: &str, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_set_channel_as_video_source( + &self, + _bridge_id: &str, + _channel_id: &str, + _video_source_id: &str, + ) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_unset_video_source(&self, _bridge_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_start_moh(&self, _bridge_id: &str, _moh_class: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_stop_moh(&self, _bridge_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_play_media(&self, _bridge_id: &str, _playback: &Playback) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_stop_media(&self, _bridge_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn bridge_start_recording(&self, _bridge_id: &str, _recording: &LiveRecording) -> AriClientResult<()> { + unimplemented!() + } +} diff --git a/crates/ari/src/ari_client/bridge/mod.rs b/crates/ari/src/ari_client/bridge/mod.rs new file mode 100644 index 0000000..eeb36ef --- /dev/null +++ b/crates/ari/src/ari_client/bridge/mod.rs @@ -0,0 +1,4 @@ +mod core; + +mod responses; +pub use responses::*; diff --git a/crates/ari/src/ari_client/bridge/responses.rs b/crates/ari/src/ari_client/bridge/responses.rs new file mode 100644 index 0000000..bcf3eab --- /dev/null +++ b/crates/ari/src/ari_client/bridge/responses.rs @@ -0,0 +1,7 @@ +use derive_getters::Getters; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Getters)] +pub struct Bridge { + id: String, +} diff --git a/crates/ari/src/ari_client/channel/core.rs b/crates/ari/src/ari_client/channel/core.rs new file mode 100644 index 0000000..88a51f2 --- /dev/null +++ b/crates/ari/src/ari_client/channel/core.rs @@ -0,0 +1,144 @@ +use std::collections::HashMap; + +use crate::*; + +impl AriClient { + pub async fn channel_answer(&self, channel_id: &str) -> AriClientResult<()> { + self.authorized_post(["channels", channel_id, "answer"], ()).await + } + + pub async fn channel_hangup(&self, channel_id: &str, reason: Reason) -> AriClientResult<()> { + self.authorized_delete(["channels", channel_id], reason).await + } + + pub async fn channel_start_ringing(&self, channel_id: &str) -> AriClientResult<()> { + self.authorized_post(["channels", channel_id, "ring"], ()).await + } + + pub async fn channel_stop_ringing(&self, channel_id: &str) -> AriClientResult<()> { + self.authorized_delete(["channels", channel_id, "ring"], ()).await + } + + pub async fn channel_send_dtmf(&self, channel_id: &str, params: SendDtmfParams<'_>) -> AriClientResult<()> { + self.authorized_post(["channels", channel_id, "dtmf"], params).await + } + + pub async fn channel_mute(&self, channel_id: &str, direction: Direction) -> AriClientResult<()> { + self.authorized_post(["channels", channel_id, "mute"], direction).await + } + + pub async fn channel_unmute(&self, channel_id: &str, direction: Direction) -> AriClientResult<()> { + self.authorized_delete(["channels", channel_id, "mute"], direction).await + } + + pub async fn channel_hold(&self, channel_id: &str) -> AriClientResult<()> { + self.authorized_post(["channels", channel_id, "hold"], ()).await + } + + pub async fn channel_unhold(&self, channel_id: &str) -> AriClientResult<()> { + self.authorized_delete(["channels", channel_id, "hold"], ()).await + } + + pub async fn channel_play_media(&self, channel_id: &str, params: PlayMediaParams<'_>) -> AriClientResult { + self.authorized_post_json_response(["channels", channel_id, "play"], params).await + } + + // SUGGESTION(gibbz00): combine with above method and mave ID optional + pub async fn channel_play_media_with_id( + &self, + channel_id: &str, + playback_id: &str, + params: PlayMediaBaseParams<'_>, + ) -> AriClientResult { + self.authorized_post_json_response(["channels", channel_id, "play", playback_id, "media"], params) + .await + } + + pub async fn channel_record(&self, channel_id: &str, params: RecordParams<'_>) -> AriClientResult { + self.authorized_post_json_response(["channels", channel_id, "record"], params).await + } + + pub async fn channel_dial(&self, channel_id: &str, params: DialParams<'_>) -> AriClientResult<()> { + self.authorized_post(["channels", channel_id, "dial"], params).await + } + + pub async fn channel_list(&self) -> AriClientResult> { + self.authorized_get(["channels"], ()).await + } + + pub async fn channel_create(&self, params: ChannelCreateParams<'_>, variables: &HashMap<&str, &str>) -> AriClientResult { + self.authorized_post_variables(["channels", "create"], params, variables).await + } + + pub async fn channel_get(self, channel_id: &str) -> AriClientResult { + self.authorized_get(["channels", channel_id], ()).await + } + + pub async fn channel_originate<'a>( + &self, + params: OriginateChannelParams<'a>, + variables: &HashMap<&str, &str>, + ) -> AriClientResult { + self.authorized_post_variables(["channels"], params, variables).await + } + + // SUGGESTION(gibbz00): combine with above method and mave ID optional + pub async fn channel_originate_with_id<'a>( + &self, + channel_id: &str, + params: OriginateChannelWithIdParams<'a>, + variables: &HashMap<&str, &str>, + ) -> AriClientResult { + self.authorized_post_variables(["channels", channel_id], params, variables).await + } + + pub async fn channel_start_moh(&self, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn channel_stop_moh(&self, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn channel_silence(&self, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn channel_unsilince(&self, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + pub async fn channel_get_variable(&self, _channel_id: &str) -> AriClientResult { + unimplemented!() + } + + pub async fn channel_set_variable(&self, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn channel_continue_in_dialplan(&self, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + /// Transfer the channel to another ARI application. + /// Same as `move` in Asterisk + pub async fn channel_transfer(&self, _channel_id: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn channel_get_rtp_statistics(&self, _channel_id: &str) -> AriClientResult { + unimplemented!() + } + + pub async fn channel_snoop(&self, _channel_id: &str) -> AriClientResult { + unimplemented!() + } + + // SUGGESTION(gibbz00): combine with above method and mave ID optional + pub async fn channel_snoop_with_id(&self, _channel_id: &str) -> AriClientResult { + unimplemented!() + } + + pub async fn channel_start_external_media(&self, _channel_id: &str) -> AriClientResult { + unimplemented!() + } +} diff --git a/crates/ari/src/ari_client/channel/mod.rs b/crates/ari/src/ari_client/channel/mod.rs new file mode 100644 index 0000000..bfae9bb --- /dev/null +++ b/crates/ari/src/ari_client/channel/mod.rs @@ -0,0 +1,7 @@ +mod core; + +mod responses; +pub use responses::*; + +mod request_params; +pub use request_params::*; diff --git a/crates/ari/src/ari_client/channel/request_params.rs b/crates/ari/src/ari_client/channel/request_params.rs new file mode 100644 index 0000000..d5970b0 --- /dev/null +++ b/crates/ari/src/ari_client/channel/request_params.rs @@ -0,0 +1,195 @@ +use serde::{ser::SerializeMap, Serialize, Serializer}; +use strum::AsRefStr; + +#[derive(Debug, Serialize)] +#[serde(tag = "direction", rename_all = "camelCase")] +pub enum Direction { + In, + Out, + Both, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OriginateChannelParams<'a> { + pub endpoint: &'a str, + pub params: OriginateParams<'a>, + pub caller_id: Option<&'a str>, + pub timeout: Option, + pub channel_id: Option<&'a str>, + pub other_channel_id: Option<&'a str>, + pub originator: Option<&'a str>, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + #[serde(serialize_with = "join_serialize")] + pub formats: &'a [&'a str], +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OriginateChannelWithIdParams<'a> { + pub endpoint: &'a str, + pub params: OriginateParams<'a>, + pub caller_id: Option<&'a str>, + pub timeout: Option, + pub other_channel_id: Option<&'a str>, + pub originator: Option<&'a str>, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + #[serde(serialize_with = "join_serialize")] + pub formats: &'a [&'a str], +} + +#[derive(Debug, Serialize)] +#[serde(untagged, rename_all = "camelCase")] +pub enum OriginateParams<'a> { + Extension { + extension: &'a str, + context: Option<&'a str>, + priority: Option, + label: Option<&'a str>, + }, + Application { + app: &'a str, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + #[serde(serialize_with = "join_serialize")] + app_args: &'a [&'a str], + }, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SendDtmfParams<'a> { + pub dtmf: &'a str, + /// in milliseconds + pub between: Option, + /// in milliseconds + pub duration: Option, + pub before: Option, + pub after: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlayMediaParams<'a> { + pub playback_id: Option<&'a str>, + #[serde(flatten)] + pub base_params: PlayMediaBaseParams<'a>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PlayMediaBaseParams<'a> { + #[serde(serialize_with = "join_serialize")] + pub media: &'a [&'a str], + pub lang: Option<&'a str>, + #[serde(rename = "offsetms")] + pub offset_ms: Option, + #[serde(rename = "skipms")] + pub skip_ms: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RecordParams<'a> { + pub name: &'a str, + pub format: &'a str, + pub max_duration_seconds: Option, + pub max_silence_seconds: Option, + pub if_exists: RecordingAction, + pub beep: bool, + pub terminate_on: RecordingTermination, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum RecordingAction { + Overwrite, + Append, + Fail, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum RecordingTermination { + None, + Any, + #[serde(rename = "*")] + Asterisk, + #[serde(rename = "#")] + Octothorpe, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DialParams<'a> { + pub caller: Option<&'a str>, + pub timeout: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelCreateParams<'a> { + pub endpoint: &'a str, + pub app: &'a str, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + #[serde(serialize_with = "join_serialize")] + pub app_args: &'a [&'a str], + pub channel_id: Option<&'a str>, + pub other_channel_id: Option<&'a str>, + pub originator: Option<&'a str>, + #[serde(skip_serializing_if = "<[_]>::is_empty")] + #[serde(serialize_with = "join_serialize")] + pub formats: &'a [&'a str], +} + +fn join_serialize(slice: &[&str], s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&slice.join(",")) +} + +// NOTE: camelCase exception +#[derive(Debug, AsRefStr)] +pub enum Reason { + Code(u16), + #[strum(serialize = "normal")] + Normal, + #[strum(serialize = "busy")] + Busy, + #[strum(serialize = "congestion")] + Congestion, + #[strum(serialize = "no_answer")] + NoAnswer, + #[strum(serialize = "timeout")] + Timeout, + #[strum(serialize = "rejected")] + Rejected, + #[strum(serialize = "unallocated")] + Unallocated, + #[strum(serialize = "normal_unspecified")] + NormalUnspecified, + #[strum(serialize = "number_incomplete")] + NumberIncomplete, + #[strum(serialize = "codec_mismatch")] + CodecMismatch, + #[strum(serialize = "interworking")] + Interworking, + #[strum(serialize = "failure")] + Failure, + #[strum(serialize = "answered_elsewhere")] + AnsweredElsewhere, +} + +impl Serialize for Reason { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + match self { + Reason::Code(code) => map.serialize_entry("reason_code", code)?, + _ => map.serialize_entry("reason", self.as_ref())?, + }; + map.end() + } +} diff --git a/crates/ari/src/ari_client/channel/responses.rs b/crates/ari/src/ari_client/channel/responses.rs new file mode 100644 index 0000000..7f679b4 --- /dev/null +++ b/crates/ari/src/ari_client/channel/responses.rs @@ -0,0 +1,46 @@ +use derive_getters::Getters; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct Channel { + id: String, + name: String, + state: String, + protocol_id: String, + caller: Caller, + connected: Caller, + accountcode: String, + dialplan: Dialplan, + creationtime: String, + language: String, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct RtpStatistics { + id: String, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct ChannelVariable { + id: String, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct Caller { + name: String, + number: String, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct Dialplan { + context: String, + exten: String, + priority: i32, + app_name: String, + app_data: String, +} diff --git a/crates/ari/src/ari_client/core.rs b/crates/ari/src/ari_client/core.rs new file mode 100644 index 0000000..4f69d4b --- /dev/null +++ b/crates/ari/src/ari_client/core.rs @@ -0,0 +1,96 @@ +use std::collections::HashMap; + +use derive_getters::Getters; +use serde::{de::DeserializeOwned, Serialize}; +use thiserror::Error; +use url::Url; + +use crate::*; + +#[derive(Debug, Getters)] +pub struct AriClient { + url: Url, + api_key: String, + inner: reqwest::Client, +} + +#[derive(Debug, Error)] +pub enum AriClientError { + #[error("encountered inner HTTP client error")] + Reqwest(#[from] reqwest::Error), +} + +impl AriClient { + pub(crate) fn new(url: Url, api_key: String) -> Self { + Self { + url, + api_key, + inner: reqwest::Client::new(), + } + } + + pub(crate) async fn authorized_get( + &self, + path: impl AsRef<[&str]>, + params: T, + ) -> AriClientResult { + let url = self.authorized_url(path, params); + let response = self.inner.get(url).send().await?.json().await?; + Ok(response) + } + + pub(crate) async fn authorized_post(&self, path: impl AsRef<[&str]>, params: T) -> AriClientResult<()> { + let url = self.authorized_url(path, params); + self.inner.post(url).send().await?; + Ok(()) + } + + pub(crate) async fn authorized_post_json_response( + &self, + path: impl AsRef<[&str]>, + params: T, + ) -> AriClientResult { + let url = self.authorized_url(path, params); + let response = self.inner.post(url).send().await?.json().await?; + Ok(response) + } + + pub(crate) async fn authorized_post_variables( + &self, + path: impl AsRef<[&str]>, + params: T, + variables: &HashMap<&str, &str>, + ) -> AriClientResult { + let url = self.authorized_url(path, params); + let response = self + .inner + .post(url) + .json(&serde_json::json!({ + "variables": variables + })) + .send() + .await? + .json() + .await?; + Ok(response) + } + + pub(crate) async fn authorized_delete(&self, path: impl AsRef<[&str]>, params: T) -> AriClientResult<()> { + let url = self.authorized_url(path, params); + self.inner.delete(url).send().await?; + Ok(()) + } + + fn authorized_url<'a, T: Serialize>(&self, path: impl AsRef<[&'a str]>, params: T) -> Url { + Authorization::build_url(&self.url, path, &self.api_key, params).expect("failed to create internally built url") + } +} + +impl Default for AriClient { + fn default() -> Self { + Self::new( + "http://localhost:8088/".parse().expect("failed to parse url"), + Authorization::api_key("asterisk", "asterisk"), + ) + } +} diff --git a/crates/ari/src/ari_client/mod.rs b/crates/ari/src/ari_client/mod.rs new file mode 100644 index 0000000..1dc7661 --- /dev/null +++ b/crates/ari/src/ari_client/mod.rs @@ -0,0 +1,16 @@ +pub(crate) type AriClientResult = Result; + +mod core; +pub use core::{AriClient, AriClientError}; + +mod bridge; +pub use bridge::*; + +mod channel; +pub use channel::*; + +mod playback; +pub use playback::*; + +mod recording; +pub use recording::*; diff --git a/crates/ari/src/ari_client/playback/core.rs b/crates/ari/src/ari_client/playback/core.rs new file mode 100644 index 0000000..1c35ca2 --- /dev/null +++ b/crates/ari/src/ari_client/playback/core.rs @@ -0,0 +1,15 @@ +use crate::*; + +impl AriClient { + pub async fn playback_get(&self, _playback_id: &str) -> AriClientResult { + unimplemented!() + } + + pub async fn playback_control(&self, _playback_id: &str, _operation: Operation) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn playback_stop(&self, _playback_id: &str) -> AriClientResult<()> { + unimplemented!() + } +} diff --git a/crates/ari/src/ari_client/playback/mod.rs b/crates/ari/src/ari_client/playback/mod.rs new file mode 100644 index 0000000..bfae9bb --- /dev/null +++ b/crates/ari/src/ari_client/playback/mod.rs @@ -0,0 +1,7 @@ +mod core; + +mod responses; +pub use responses::*; + +mod request_params; +pub use request_params::*; diff --git a/crates/ari/src/ari_client/playback/request_params.rs b/crates/ari/src/ari_client/playback/request_params.rs new file mode 100644 index 0000000..7fba6d3 --- /dev/null +++ b/crates/ari/src/ari_client/playback/request_params.rs @@ -0,0 +1,11 @@ +use serde::Serialize; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Operation { + Restart, + Pause, + Unpause, + Reverse, + Forward, +} diff --git a/crates/ari/src/ari_client/playback/responses.rs b/crates/ari/src/ari_client/playback/responses.rs new file mode 100644 index 0000000..714790a --- /dev/null +++ b/crates/ari/src/ari_client/playback/responses.rs @@ -0,0 +1,8 @@ +use derive_getters::Getters; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct Playback { + id: String, +} diff --git a/crates/ari/src/ari_client/recording/core.rs b/crates/ari/src/ari_client/recording/core.rs new file mode 100644 index 0000000..ee3a59f --- /dev/null +++ b/crates/ari/src/ari_client/recording/core.rs @@ -0,0 +1,50 @@ +use crate::*; + +impl AriClient { + pub async fn live_recording_get(&self, _recording_name: &str) -> AriClientResult { + unimplemented!() + } + + pub async fn live_recording_discard(&self, _recording_name: &str) -> AriClientResult<()> { + unimplemented!() + } + + // TODO: explore if it's possible to return a StoredRecording + pub async fn live_recording_stop(&self, _recording_name: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn live_recording_pause(&self, _recording_name: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn live_recording_resume(&self, _recording_name: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn live_recording_mute(&self, _recording_name: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn live_recording_unmute(&self, _recording_name: &str) -> AriClientResult<()> { + unimplemented!() + } +} + +impl AriClient { + pub async fn stored_recording_list(&self, _recording_name: &str) -> AriClientResult> { + unimplemented!() + } + + pub async fn stored_recording_get(&self, _recording_name: &str) -> AriClientResult { + unimplemented!() + } + + pub async fn stored_recording_delete(&self, _recording_name: &str) -> AriClientResult<()> { + unimplemented!() + } + + pub async fn stored_recording_download(&self, _recording_name: &str) -> AriClientResult<&[u8]> { + unimplemented!() + } +} diff --git a/crates/ari/src/ari_client/recording/mod.rs b/crates/ari/src/ari_client/recording/mod.rs new file mode 100644 index 0000000..eeb36ef --- /dev/null +++ b/crates/ari/src/ari_client/recording/mod.rs @@ -0,0 +1,4 @@ +mod core; + +mod responses; +pub use responses::*; diff --git a/crates/ari/src/ari_client/recording/responses.rs b/crates/ari/src/ari_client/recording/responses.rs new file mode 100644 index 0000000..572070f --- /dev/null +++ b/crates/ari/src/ari_client/recording/responses.rs @@ -0,0 +1,16 @@ +use derive_getters::Getters; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct LiveRecording { + id: String, + name: String, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct StoredRecording { + id: String, + format: String, +} diff --git a/crates/ari/src/asterisk.rs b/crates/ari/src/asterisk.rs new file mode 100644 index 0000000..2a2b0a0 --- /dev/null +++ b/crates/ari/src/asterisk.rs @@ -0,0 +1,153 @@ +use std::{result::Result, time::Duration}; + +use futures_util::{SinkExt, StreamExt, TryStreamExt}; +use rand::Rng; +use serde::Serialize; +use thiserror::Error; +use tokio::{sync::mpsc::UnboundedReceiver, time::interval}; +use tokio_tungstenite::{connect_async, tungstenite}; +use url::Url; + +use crate::*; + +#[derive(Debug, Error)] +pub enum AsteriskError { + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + #[error("unsupported scheme, expected 'http' or 'https', found: '{}'", .0)] + UnsupportedScheme(String), + #[error("failed to connect")] + WebSocketConnect(tokio_tungstenite::tungstenite::Error), +} + +pub struct Asterisk; + +impl Asterisk { + /// Connect to the Asterisk, return an ARI request client and an WebSocket event stream. + /// + /// Spawns a [`tokio::task`] that connects to the Asterisk + /// WebSocket endpoint to immideatedly listen to incoming [`AsteriskEvent`] + /// (`crate::Event`)s. These may be listened to by polling the returned + /// [`tokio::sync::mpsc::UnboundedReceiver`] stream. + /// + /// Event listener task is stopped if the listener stream is dropped, + /// when WebSocket connection toward the Astbrisk is dropped, be it + /// gracefully or unintentionally. But also when any + /// + /// # Arguments + /// + /// - `url` should end in `/`, `ari/` will be appended to it. + /// + /// # Panics + /// + /// - If called outside a Tokio runtime. + /// - If if the library fails to deserialize incoming asterisk messages + pub async fn connect( + url: impl AsRef, + app_name: impl AsRef, + username: impl AsRef, + password: impl AsRef, + ) -> Result<(AriClient, UnboundedReceiver), AsteriskError> { + let url = url.as_ref().parse::()?.join("ari/")?; + + let api_key = Authorization::api_key(username.as_ref(), password.as_ref()); + + let ws_url = Self::build_ws_url(&url, &api_key, app_name.as_ref())?; + + let request_client = AriClient::new(url, api_key); + + let event_listener = Self::connect_ws(&ws_url).await?; + + Ok((request_client, event_listener)) + } + + fn build_ws_url(base_url: &Url, api_key: &str, app_name: &str) -> Result { + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct AsteriskWebSocketParams<'a> { + #[serde(rename = "app")] + app_name: &'a str, + subscribe_all: bool, + } + + let mut ws_url = base_url.join("events")?; + let scheme = match ws_url.scheme() { + "http" => "ws", + "https" => "wss", + other => Err(AsteriskError::UnsupportedScheme(other.to_string()))?, + }; + ws_url.set_scheme(scheme).expect("invalid url scheme"); + + let ws_url = Authorization::build_url( + &ws_url, + [], + api_key, + AsteriskWebSocketParams { + app_name, + subscribe_all: true, + }, + )?; + + Ok(ws_url) + } + + async fn connect_ws(ws_url: &Url) -> Result, AsteriskError> { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let (ws_stream, _) = connect_async(ws_url).await.map_err(AsteriskError::WebSocketConnect)?; + + tokio::task::spawn(async move { + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + let mut interval = interval(Duration::from_millis(5000)); + + loop { + tokio::select! { + message_result = ws_receiver.try_next() => { + let Ok(Some(message)) = message_result else { + // IMPROVEMENT: might be wiser to propagate the + // errors to the event consumer + break; + }; + + match message { + tungstenite::Message::Text(_) => { + let event_result = serde_json::from_slice(message.into_data().as_slice()) + .expect("failed to deserialize asterisk event"); + if tx.send(event_result).is_err() { + // Assume that tx has been dropped + break; + } + } + tungstenite::Message::Ping(data) => { + if ws_sender.send(tungstenite::Message::Pong(data)).await.is_err() { + // IMPROVEMENT: might be wiser to propagate the + // errors to the event consumer + break; + } + } + tungstenite::Message::Pong(_) => { }, + tungstenite::Message::Close(_frame) => { + break; + }, + tungstenite::Message::Frame(_) => { + unreachable!("raw frame not supposed to be received when reading incoming messages") + } + tungstenite::Message::Binary(_) => { + unreachable!("asterisk should send data marked using text payloads") + } + } + } + _ = interval.tick() => { + // every 5 seconds we are sending ping to keep connection alive + // https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html + let random_bytes = rand::thread_rng().gen::<[u8; 32]>().to_vec(); + if ws_sender.send(tungstenite::Message::Ping(random_bytes)).await.is_err() { + break; + } + } + } + } + }); + + Ok(rx) + } +} diff --git a/crates/ari/src/authorization.rs b/crates/ari/src/authorization.rs new file mode 100644 index 0000000..3e50155 --- /dev/null +++ b/crates/ari/src/authorization.rs @@ -0,0 +1,79 @@ +use serde::Serialize; +use url::Url; + +pub struct Authorization; + +#[derive(Serialize)] +struct AuthorizedRequestParams<'a, T> { + api_key: &'a str, + #[serde(flatten)] + inner: T, +} + +impl Authorization { + pub(crate) fn api_key(username: &str, password: &str) -> String { + format!("{}:{}", username, password) + } + + pub(crate) fn build_url<'a, T: Serialize>( + url: &Url, + path: impl AsRef<[&'a str]>, + api_key: &str, + params: T, + ) -> Result { + let mut url = url.join(&path.as_ref().join("/"))?; + + Self::add_query_parameters(&mut url, api_key, params); + + Ok(url) + } + + fn add_query_parameters(url: &mut Url, api_key: &str, params: T) { + let authorized_request_params = AuthorizedRequestParams { api_key, inner: params }; + + let query_string = serde_qs::to_string(&authorized_request_params).expect("failed to serialize query parameters"); + + url.set_query(Some(&query_string)); + } +} + +#[cfg(test)] +mod tests { + use crate::*; + + #[test] + fn builds_url() { + let url = "http://localhost:8080/".parse().unwrap(); + let api_key = Authorization::api_key("asterisk", "asterisk"); + + let expected = "http://localhost:8080/channel?api_key=asterisk%3Aasterisk&media=sound%3Ahello&lang=en"; + let actual = Authorization::build_url( + &url, + ["channel"], + &api_key, + PlayMediaParams { + playback_id: None, + base_params: PlayMediaBaseParams { + media: &["sound:hello"], + lang: Some("en"), + offset_ms: None, + skip_ms: None, + }, + }, + ) + .unwrap(); + + assert_eq!(expected, actual.as_str()) + } + + #[test] + fn builds_url_with_newtype() { + let url = "http://localhost:8080/".parse().unwrap(); + let api_key = Authorization::api_key("asterisk", "asterisk"); + + let expected = "http://localhost:8080/channel?api_key=asterisk%3Aasterisk"; + let actual = Authorization::build_url(&url, ["channel"], &api_key, ()).unwrap(); + + assert_eq!(expected, actual.as_str()) + } +} diff --git a/crates/ari/src/event.rs b/crates/ari/src/event.rs new file mode 100644 index 0000000..5152670 --- /dev/null +++ b/crates/ari/src/event.rs @@ -0,0 +1,114 @@ +use chrono::{DateTime, Utc}; +use derive_getters::Getters; +use derive_more::derive::Deref; +use serde::Deserialize; + +use crate::*; + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum AsteriskEvent { + StasisStart(Box>), + StasisEnd(Event), + ChannelCreated(Event), + ChannelDestroyed(Event), + ChannelVarset(Event), + ChannelHangupRequest(Event), + ChannelDialplan(Event), + ChannelStateChange(Event), + ChannelDtmfReceived(Event), + DeviceStateChanged(Event), + #[serde(other)] + Unknown, +} + +#[derive(Debug, Deserialize, Getters, Deref)] +#[serde(rename_all = "snake_case")] +pub struct Event { + asterisk_id: String, + application: String, + timestamp: DateTime, + #[deref] + #[getter(skip)] + #[serde(flatten)] + data: D, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct StasisStart { + args: Vec, + channel: Channel, + replace_channel: Option, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct StasisEnd { + channel: Channel, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct ChannelCreated { + channel: Option, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct ChannelDestroyed { + cause: i32, + cause_txt: String, + channel: Channel, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct ChannelVarset { + variable: String, + value: String, + channel: Option, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct ChannelHangupRequest { + soft: Option, + cause: i32, + channel: Channel, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct ChannelDialplan { + dialplan_app: String, + dialplan_app_data: String, + channel: Channel, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct DeviceStateChanged { + device_state: DeviceState, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct ChannelStateChange { + channel: Channel, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct ChannelDtmfReceived { + digit: String, + duration_ms: i32, + channel: Channel, +} + +#[derive(Debug, Deserialize, Getters)] +#[serde(rename_all = "snake_case")] +pub struct DeviceState { + name: String, + state: String, +} diff --git a/crates/ari/src/lib.rs b/crates/ari/src/lib.rs new file mode 100644 index 0000000..1fcfacf --- /dev/null +++ b/crates/ari/src/lib.rs @@ -0,0 +1,11 @@ +mod asterisk; +pub use asterisk::Asterisk; + +mod ari_client; +pub use ari_client::*; + +mod authorization; +pub(crate) use authorization::Authorization; + +mod event; +pub use event::AsteriskEvent;