diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e7b9f5..12d4ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [0.1.7] + +- **Talkgroups** is a menu on each system's card: **Every talkgroup**, **Only some** (with + **Except these**) or **All but some**, each list shown when it applies. Lists take ranges + (`1000-1099`) as well as numbers and patterns, separated by commas or spaces, and the plugin + won't start with an entry that's none of them, saying which. **Breaking:** lists saved without a + menu choice no longer apply, and Trunk Recorder's `broadcastifyAllow` / `broadcastifyDeny` names + (and `…Whitelist` / `…Blacklist`) are no longer read: choose the menu on each system's card. +- Built with trunk-recorder-plugin 0.8.0 (its `Talkgroups`), for Trunk Recorder Pro v0.2.10. + ## [0.1.6] - Built with trunk-recorder-plugin 0.5.0, for Trunk Recorder Pro v0.2.0: systems are numbered diff --git a/Cargo.lock b/Cargo.lock index aa32da2..e728aba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "broadcastify" -version = "0.1.6" +version = "0.1.7" dependencies = [ "schemars", "serde", @@ -372,9 +372,9 @@ dependencies = [ [[package]] name = "trunk-recorder-plugin" -version = "0.5.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a7504b42cca72ec6457beb7ac279f3a4fbe16054f86be57eb1e28a70dd6394d" +checksum = "9da2b5da860ea26119864505f20aa59c0628c8d7959bdef9c794ec1868fc21c1" dependencies = [ "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 26079c0..e0d4386 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broadcastify" -version = "0.1.6" +version = "0.1.7" edition = "2024" description = "Uploads recorded calls to Broadcastify Calls." license = "GPL-3.0-or-later" @@ -10,7 +10,7 @@ authors = ["Luke Berndt "] publish = false [dependencies] -trunk-recorder-plugin = "0.5.0" +trunk-recorder-plugin = "0.8.0" schemars = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -20,3 +20,4 @@ ureq = "3" [profile.release] strip = true lto = true + diff --git a/README.md b/README.md index 905b63c..d7df351 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,17 @@ For each system: |---|---| | **API key** | Your node's upload key. Leave it empty to not upload that system. | | **System ID** | The system's number on Broadcastify Calls. | -| **Only these talkgroups** | Upload these talkgroups and no others. Leave it empty to upload all of them. | -| **Not these talkgroups** | Never upload these talkgroups. | +| **Talkgroups** | **Every talkgroup**, **Only some** or **All but some**. | +| **Only these** | With **Only some**: upload these talkgroups and no others. | +| **Except these** | With **Only some** or **All but some**: never upload these. | | **Sites of their own** | A site of the system that Broadcastify Calls knows as a system of its own: its name (as under Sites in Setup), its system ID, and its API key if not the system's. Its calls go there; the system's other sites' go to the system's. | -Talkgroups can be numbers or patterns: `*` stands for any digits and `?` for -one, so `507*` is every talkgroup starting with 507 and `12???` is every -five-digit one starting with 12. When both lists are set, a talkgroup has to -be in the first and not in the second. +Talkgroups can be numbers, ranges or patterns: `1000-1099` is every talkgroup +from 1000 to 1099, `*` stands for any digits and `?` for one, so `507*` is +every talkgroup starting with 507 and `12???` every five-digit one starting +with 12. Separate them with commas or spaces. The plugin won't start with an +entry that's none of these, and says which. Lists saved before the menu still +apply (the log says so); choose the menu once to see them on the card. ## What it does with calls @@ -76,9 +79,9 @@ Broadcastify settings; add the others' system IDs here. Trunk Recorder's settings can be pasted in as they are: `broadcastifyCallsServer`, `broadcastifyOTA` and `broadcastifySslVerifyDisable` -are the plugin's settings, and each system's `broadcastifyApiKey`, -`broadcastifySystemId`, `broadcastifyAllow` and `broadcastifyDeny` (or the -older `…Whitelist` / `…Blacklist` names) are its settings for that system. +are the plugin's settings, and each system's `broadcastifyApiKey` and +`broadcastifySystemId` are its settings for that system. Its talkgroup lists +are set with the **Talkgroups** menu. Trunk Recorder systems that are sites of one system (`multiSite`) are one system here: give each site's system ID under **Sites of their own**. diff --git a/examples/settings.json b/examples/settings.json index c912acb..646648b 100644 --- a/examples/settings.json +++ b/examples/settings.json @@ -1,4 +1,4 @@ { "config": { "server": "https://api.broadcastify.com/call-upload", "talkerAliases": true }, - "systems": { "county": { "apiKey": "your-broadcastify-api-key", "systemId": 1234, "talkgroupDeny": ["99*"] } } + "systems": { "county": { "apiKey": "your-broadcastify-api-key", "systemId": 1234, "talkgroupMode": "allBut", "talkgroupDeny": ["99*"] } } } diff --git a/src/main.rs b/src/main.rs index 1b6db38..6226e12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,9 @@ use std::time::Duration; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use trunk_recorder_plugin::filter::patterns; -use trunk_recorder_plugin::{Attempt, CallQueue, ConcludedCall, Host, Manifest, Plugin, QueueOptions, Setup, State, TalkgroupFilter, format, topic}; +use trunk_recorder_plugin::{ + Attempt, CallQueue, ConcludedCall, Host, Manifest, Plugin, QueueOptions, Setup, State, TalkgroupFilter, Talkgroups, format, topic, +}; use upload::{Upload, Uploader}; @@ -59,18 +60,9 @@ struct SystemConfig { #[serde(alias = "broadcastifySystemId")] #[schemars(extend("x-required" = true))] system_id: Option, - /// Only these talkgroups - /// - /// Upload these talkgroups and no others: numbers, or patterns like 507* (* stands for any digits, ? for one). Leave it empty to upload every talkgroup. - #[serde(alias = "broadcastifyAllow", alias = "broadcastifyWhitelist", alias = "talkgroupWhitelist", deserialize_with = "patterns")] - #[schemars(with = "Vec")] - talkgroup_allow: Vec, - /// Not these talkgroups - /// - /// Never upload these talkgroups: numbers or patterns, as above. - #[serde(alias = "broadcastifyDeny", alias = "broadcastifyBlacklist", alias = "talkgroupBlacklist", deserialize_with = "patterns")] - #[schemars(with = "Vec")] - talkgroup_deny: Vec, + // The talkgroups to upload: a menu, and the lists it asks for. + #[serde(flatten)] + talkgroups: Talkgroups, /// Sites of their own /// /// A site of this system that Broadcastify Calls knows as a system of its own: its calls go there instead. @@ -169,7 +161,7 @@ impl Plugin for Broadcastify { host.warn(format!("not checking the certificate of {server} for {}", s.short_name)); } let uploader = uploaders.entry((server.clone(), skip)).or_insert_with(|| Arc::new(Uploader::new(&server, skip))).clone(); - let filter = TalkgroupFilter::new(&c.talkgroup_allow, &c.talkgroup_deny); + let filter = c.talkgroups.filter_for(&s.short_name)?; let filtered = if filter.is_empty() { String::new() } else { format!(", talkgroups: {}", filter.describe()) }; let lane = lane_of(&server); let to = if lane.is_empty() { String::new() } else { format!(" to {lane}") }; @@ -369,7 +361,7 @@ mod tests { #[test] fn talkgroup_filters_and_trunk_recorders_setting_names() { let (dir, server) = (testing::temp_dir("bcfy"), broadcastify()); - let system = json!({ "broadcastifyApiKey": "good", "broadcastifySystemId": 42, "broadcastifyAllow": ["10*", 205], "broadcastifyDeny": ["109"] }); + let system = json!({ "broadcastifyApiKey": "good", "broadcastifySystemId": 42, "talkgroupMode": "only", "talkgroupAllow": ["10*", 205], "talkgroupDeny": ["109"] }); let mut h = testing::hello(&dir, json!({ "broadcastifyCallsServer": format!("{}/call-upload", server.url()), "broadcastifyOTA": false })); h.systems[0].config = system; let out = testing::run::([ @@ -388,6 +380,34 @@ mod tests { assert!(posts[0].form_field("srcId_alias").is_none()); } + /// The menu: only a range, except one of it; all but some; and an entry that isn't a talkgroup stops it starting. + #[test] + fn talkgroups_chosen_in_the_menu() { + let (dir, server) = (testing::temp_dir("bcfy"), broadcastify()); + let sent = |system: Value, tgs: &[u32]| { + let mut msgs = vec![hello(&dir, server.url(), system)]; + msgs.extend(tgs.iter().map(|&tg| HostMessage::CallConcluded(testing::call(&dir, "sys1", tg)))); + let out = testing::run::(msgs); + let mut ok: Vec = out + .results() + .iter() + .filter(|r| r.1 == Outcome::Ok) + .map(|r| r.0.split('/').next_back().unwrap()[..4].trim_end_matches('-').parse().unwrap()) + .collect(); + ok.sort(); + (ok, out) + }; + let only = json!({ "apiKey": "good", "systemId": 42, "talkgroupMode": "only", "talkgroupAllow": ["1000 - 1099"], "talkgroupDeny": [1050] }); + let (ok, out) = sent(only, &[1000, 1050, 1099, 2000]); + assert_eq!(ok, [1000, 1099]); + assert!(out.logs().iter().any(|l| l.1.contains("talkgroups: only 1000-1099; except 1050")), "{:?}", out.logs()); + let but = json!({ "apiKey": "good", "systemId": 42, "talkgroupMode": "allBut", "talkgroupAllow": ["1000-1099"], "talkgroupDeny": ["20??"] }); + assert_eq!(sent(but, &[1000, 2001, 3000]).0, [1000, 3000], "the hidden allow list doesn't apply"); + let (_, out) = sent(json!({ "apiKey": "good", "systemId": 42, "talkgroupMode": "only", "talkgroupAllow": ["10OO"] }), &[]); + assert_eq!(out.exit_code, EXIT_CONFIG); + assert!(out.status().unwrap().1.starts_with("sys1's talkgroups: \"10OO\" isn't a talkgroup"), "{:?}", out.status()); + } + #[test] fn encrypted_calls_are_skipped() { let (dir, server) = (testing::temp_dir("bcfy"), broadcastify()); @@ -433,8 +453,9 @@ mod tests { fn the_settings_form() { let m = trunk_recorder_plugin::describe::(); let s = m.system_config.unwrap(); - assert_eq!(s["properties"]["talkgroupAllow"]["type"], "array"); - assert_eq!(s["properties"]["talkgroupAllow"]["items"]["type"], "string"); + assert_eq!(s["properties"]["talkgroupMode"]["enum"], json!(["all", "only", "allBut"])); + assert_eq!(s["properties"]["talkgroupAllow"]["x-when"], json!({ "talkgroupMode": "only" })); + assert_eq!(s["x-order"], json!(["apiKey", "systemId", "talkgroupMode", "talkgroupAllow", "talkgroupDeny", "sites"])); assert_eq!(s["properties"]["systemId"]["type"], "integer"); assert_eq!(m.config.unwrap()["properties"]["talkerAliases"]["default"], true); } @@ -477,6 +498,7 @@ mod tests { kind: "p25".into(), config: json!({ "apiKey": "good", "systemId": 42 }), settings: json!({ "server": format!("{}/call-upload", other.url()), "talkerAliases": false }), + ..Default::default() }); let mut one = testing::call(&dir, "sys1", 101); one.call.src_list[0].tag_ota = "ENGINE 1".into();