Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -10,7 +10,7 @@ authors = ["Luke Berndt <lukekb@gmail.com>"]
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"
Expand All @@ -20,3 +20,4 @@ ureq = "3"
[profile.release]
strip = true
lto = true

21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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**.

Expand Down
2 changes: 1 addition & 1 deletion examples/settings.json
Original file line number Diff line number Diff line change
@@ -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*"] } }
}
58 changes: 40 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -59,18 +60,9 @@ struct SystemConfig {
#[serde(alias = "broadcastifySystemId")]
#[schemars(extend("x-required" = true))]
system_id: Option<u32>,
/// 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<String>")]
talkgroup_allow: Vec<String>,
/// 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<String>")]
talkgroup_deny: Vec<String>,
// 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.
Expand Down Expand Up @@ -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}") };
Expand Down Expand Up @@ -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::<Broadcastify>([
Expand All @@ -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::<Broadcastify>(msgs);
let mut ok: Vec<u32> = 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());
Expand Down Expand Up @@ -433,8 +453,9 @@ mod tests {
fn the_settings_form() {
let m = trunk_recorder_plugin::describe::<Broadcastify>();
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);
}
Expand Down Expand Up @@ -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();
Expand Down
Loading