1use crate::device::device::{DeviceTag, TagType};
2use ip_in_subnet::iface_in_subnet;
3use lazy_static::lazy_static;
4use log::{debug, error, trace};
5use phonenumber::country;
6use phonenumber::country::Id;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11lazy_static! {
12 static ref PHONE_CLEANER: Regex = Regex::new(r"(?:\+|<sip:)?([^>:]+)(?::.*|>.*)?").unwrap();
14 static ref REQUEST_PLUS: Regex = Regex::new(r"\+\[request\.(\w+)\]").unwrap();
15 static ref APP_PLUS: Regex = Regex::new(r"\+\[app\.(\w+)\]").unwrap();
16 static ref REQUEST: Regex = Regex::new(r"\[request\.(\w+)\]").unwrap();
17 static ref APP: Regex = Regex::new(r"\[app\.(\w+)\]").unwrap();
18}
19
20#[derive(Serialize, Deserialize, Clone, PartialEq)]
21#[serde(rename_all = "lowercase")]
22pub enum NumberFormat {
23 #[serde(rename = "national")]
24 National,
25 #[serde(rename = "e164")]
26 E164,
27 #[serde(rename = "e164p")]
28 E164Plus,
29}
30
31pub fn clean_str_parse_country(country_code: &str, str: &str) -> String {
37 trace!("Cleaning phone number '{}' with country code '{}'", str, country_code);
38
39 let id = match country_code.parse() {
40 Ok(id) => id,
41 Err(e) => {
42 debug!("Invalid country code '{}', defaulting to GB: {}", country_code, e);
43 country::GB
44 }
45 };
46
47 clean_str(id, str)
48}
49
50pub fn clean_str(country: Id, str: &str) -> String {
54 trace!("Cleaning phone number '{}' for country {:?}", str, country);
55
56 let clean = match PHONE_CLEANER.captures(str) {
58 Some(caps) => {
59 caps.get(1).map_or(str, |m| m.as_str())
61 },
62 None => {
63 debug!("Regex pattern didn't match for '{}', using as-is", str);
64 str
65 }
66 };
67
68 trace!("Extracted number part: '{}'", clean);
69 parse_number(country, clean.to_string())
70}
71
72pub fn format_number(str: &str, country_code: &str, format: &NumberFormat) -> String {
79 trace!("Formatting '{}' with country code '{}' using format {:?}", str, country_code, format);
80
81 let clean_str = clean_str_parse_country(country_code, str);
82
83 match format {
84 NumberFormat::National => {
85 let result = national_number(&clean_str, country_code);
86 debug!("Formatted '{}' to national format: '{}'", str, result);
87 result
88 },
89 NumberFormat::E164 => {
90 debug!("Formatted '{}' to E164 format: '{}'", str, clean_str);
91 clean_str
92 },
93 NumberFormat::E164Plus => {
94 let result = format!("+{}", clean_str);
95 debug!("Formatted '{}' to E164Plus format: '{}'", str, result);
96 result
97 },
98 }
99}
100
101fn national_number(str: &str, country_code: &str) -> String {
103 trace!("Converting '{}' to national format for country '{}'", str, country_code);
104
105 let id = match country_code.parse() {
106 Ok(id) => id,
107 Err(e) => {
108 debug!("Invalid country code '{}', defaulting to GB: {}", country_code, e);
109 country::GB
110 }
111 };
112
113 parse_number(id, str.to_string())
114}
115
116fn parse_number(country: Id, clean: String) -> String {
118 trace!("Parsing '{}' for country {:?}", clean, country);
119
120 match phonenumber::parse(Some(country), &clean) {
121 Ok(number) => {
122 let valid = phonenumber::is_valid(&number);
123 if valid {
124 let result = format!("{}{}", number.code().value(), number.national().value());
125 trace!("Successfully parsed number: '{}'", result);
126 result
127 } else {
128 debug!("Number '{}' parsed but not valid, returning as-is", clean);
129 clean
130 }
131 }
132 Err(e) => {
133 debug!("Failed to parse '{}': {}", clean, e);
134 clean
135 }
136 }
137}
138
139pub fn replace_placeholders_with_formatting(
148 input: &str,
149 device_tags: &HashMap<String, DeviceTag>,
150 country_code: &str,
151 format: &NumberFormat
152) -> String {
153 debug!("Replacing placeholders in '{}' with country_code '{}', format {:?}",
154 input, country_code, format);
155
156 let mut result = input.to_string();
157
158 let replacements = find_tag_replacements(
160 &result,
161 device_tags,
162 country_code,
163 format,
164 true
165 );
166
167 for (placeholder, value) in replacements {
168 result = result.replace(&placeholder, &value);
169 }
170
171 let replacements = find_tag_replacements(
173 &result,
174 device_tags,
175 country_code,
176 format,
177 false
178 );
179
180 for (placeholder, value) in replacements {
181 result = result.replace(&placeholder, &value);
182 }
183
184 debug!("Replacement result: '{}'", result);
185 result
186}
187
188fn find_tag_replacements(
190 input: &str,
191 device_tags: &HashMap<String, DeviceTag>,
192 country_code: &str,
193 format: &NumberFormat,
194 with_plus: bool
195) -> Vec<(String, String)> {
196 let placeholder_type = if with_plus { "plus-prefixed" } else { "standard" };
197 trace!("Finding {} placeholders in '{}'", placeholder_type, input);
198
199 let mut replacements = Vec::new();
200
201 if with_plus {
203 for cap in REQUEST_PLUS.captures_iter(input) {
204 let placeholder = cap[0].to_string();
205 let tag_name = &cap[1];
206
207 let key = format!("request.{}", tag_name);
208 if let Some(tag) = device_tags.get(&key) {
209 if tag.tag_type == TagType::Session {
210 let formatted_value = format_number(&tag.value, country_code, format);
211 trace!("Found request+ tag: '{}' → '{}'", placeholder, formatted_value);
212 replacements.push((placeholder, formatted_value));
213 }
214 }
215 }
216 } else {
217 for cap in REQUEST.captures_iter(input) {
218 let placeholder = cap[0].to_string();
219 let tag_name = &cap[1];
220
221 let key = format!("request.{}", tag_name);
222 if let Some(tag) = device_tags.get(&key) {
223 if tag.tag_type == TagType::Session {
224 let formatted_value = format_number(&tag.value, country_code, format);
225 trace!("Found request tag: '{}' → '{}'", placeholder, formatted_value);
226 replacements.push((placeholder, formatted_value));
227 }
228 }
229 }
230 }
231
232 if with_plus {
234 for cap in APP_PLUS.captures_iter(input) {
235 let placeholder = cap[0].to_string();
236 let tag_name = &cap[1];
237
238 let key = format!("app.{}", tag_name);
239 if let Some(tag) = device_tags.get(&key) {
240 if tag.tag_type == TagType::Global {
241 let formatted_value = format_number(&tag.value, country_code, format);
242 trace!("Found app+ tag: '{}' → '{}'", placeholder, formatted_value);
243 replacements.push((placeholder, formatted_value));
244 }
245 }
246 }
247 } else {
248 for cap in APP.captures_iter(input) {
249 let placeholder = cap[0].to_string();
250 let tag_name = &cap[1];
251
252 let key = format!("app.{}", tag_name);
253 if let Some(tag) = device_tags.get(&key) {
254 if tag.tag_type == TagType::Global {
255 let formatted_value = format_number(&tag.value, country_code, format);
256 trace!("Found app tag: '{}' → '{}'", placeholder, formatted_value);
257 replacements.push((placeholder, formatted_value));
258 }
259 }
260 }
261 }
262
263 replacements
264}
265
266pub fn ip_in_subnet(ip: &str, subnet: &str) -> bool {
275 trace!("Checking if IP '{}' is in subnet '{}'", ip, subnet);
276
277 if ip == subnet {
279 debug!("IP '{}' matches subnet '{}' directly", ip, subnet);
280 return true;
281 }
282
283 match iface_in_subnet(ip, subnet) {
285 Ok(result) => {
286 debug!("IP '{}' in subnet '{}': {}", ip, subnet, result);
287 result
288 },
289 Err(e) => {
290 error!("Failed to check if IP '{}' is in subnet '{}': {}", ip, subnet, e);
291 false
292 }
293 }
294}