Инициализируем звонок аппаратного IP телефона с Windows. Знакомство с RUST

Как-то раз я заметил, что при клике на ссылки типа tel: винда предлагает мне сделать вызов через skype. Но я не пользуюсь скайпом! А вот физическим IP телефоном очень даже пользуюсь. И как порой бывает, ты лениво набираешь на нём номер с какого-то сайта, а ещё умудряешься набрать его с ошибкой… Как было бы круто отправлять вызов одним кликом на ссылку? И как оказалось, это вполне реально, ведь такие аппараты, как Fanvil, поддерживают эмуляцию ввода через HTTP запрос. Осталось только написать небольшую программку, которая ловит вызов определённых ссылок и передаёт вызываемый номер аппарату.

Выбор языка программирования

На чём реализовать программу? Мне сразу же посоветовали python. Но чёрт побери, зачем мне ради такой мелочи ставить в винду интерпретатор? Я принципиально захотел запилить прогу в виде exe’шника, которому не нужны какие-то особенные зависимости. И тут я вспомнил, что уже давно хотел познакомиться с молодым и перспективным конкурентом C++, под названием Rust.
Повод был просто шикарный, потому как желаемая программа весьма проста и не требует глубокого погружения, что как раз подходит для первого боевого опыта, вместо банального “hello world”. =)

Подготовка среды

И вот тут я, пожалуй, не стану повторять кучу других инструкций, а просто дам ссылочку на ту, с которой у меня всё получилось. Собственно, там же и курс по изучению самого языка.

Собственно, код.

Что делает сисадмин, когда очень нужно написать программу на незнакомом языке? Правильно! Копипастинг – наше всё. :) Но в данном случае, мне круто помог ChatGPT и где-то с 20-й попытки получилось выудить у него практически рабочий код. По частям конечно, но тем не менее, от себя я практически ничего не добавлял. Разве что исправлял фрагменты, на которые явно жаловался компилятор.

В результате получилось вот это:

#![windows_subsystem = "windows"] // Объявляем, что мы на винде. В ином случае получится консольное приложение.

use std::env;
use reqwest;
use regex::Regex;
extern crate toml;
use std::fs;
use toml::Value;
use native_dialog::MessageType;
use native_dialog::MessageDialog;
use std::error::Error;

fn show_error(error_message: &str) { // Функция для отображения ошибок
    let result = MessageDialog::new()
        .set_type(MessageType::Error)
        .set_title("Ошибка")
        .set_text(error_message)
        .show_alert();
    if let Err(err) = result {
        eprintln!("Ошибка при отображении диалогового окна: {:?}", err);
    }
}

#[tokio::main] // тут что-то про асинхронность. :) 
async fn main() ->  Result<(), Box<dyn Error>> {
    if let Ok(current_exe) = env::current_exe() { // тут мы проверяем, где находится исполнимый файл
        if let Some(parent_dir) = current_exe.parent() { // для того, чтобы....
            let path = format!("{}\\config.toml", parent_dir.to_string_lossy()); // Подхватить лежащий рядом конфиг.
            let config_contents = fs::read_to_string(path)?; // Читаем файл
            let config: Value = toml::from_str(&config_contents)?; // Парсим
            let url2 = config["url"].to_string(); // Используем то, что там написано.
            let args: Vec<String> = env::args().collect();
            if args.len() > 1 {
                let re = Regex::new(r"\D+").unwrap();
                let phone_number = re.replace_all(&args[1], "");
                let url = format!("{}{}",url2.trim_matches('\"'), phone_number);
                let response = reqwest::get(&url).await?; // Отправляем номер методом GET
                let response_text = response.text().await?;
            } else {
                show_error("Нет номера телефона для обработки!"); // Внезапно, прогу запустили без параметров.
            }

        }
    }   
    Ok(()) // Всем спасибо, все свободны.
}

Далее: Cargo.toml. Тут указывается некоторая инфа о нашей программе, а так-же зависимости с версиями.

[package]
name = "caller"
version = "0.1.0"
edition = "2021"
metadata = "icon.ico"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = "0.11"
tokio = { version = "1", features = ["full"] }
log = "0.4"
regex = "1.4"
toml = "0.8.0"
native-dialog = "0.6.4"

[build-dependencies]
winres = "0.1"

[build]
target = "x86_64-pc-windows-gnu"

И тут по идее всё должно собраться, но если что-то пошло не так – архив с исходниками можно качнуть тут. Ну а если собирать самостоятельно лень, то тут лежит архив с готовой софтиной.

Привязываем программу к ссылкам типа tel: и sip:

Тут, как ни странно, я столкнулся с самой непонятной частью моей затеи. Я просто не имел никакого понятия, как сообщить винде, что нужно открывать ссылки моей кастомной программой. Но имея на компе софт, который уже это делает (кажется это был microsip), я решил порыться в реестре и поискать записи, при помощи которых это может работать. В результате получилось следующее:

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\caller]
@="C:\\Program Files\\Fanvil\\"

[HKEY_CURRENT_USER\Software\caller\Capabilities]
"ApplicationDescription"="Phone Adaptor"
"ApplicationName"="caller"

[HKEY_CURRENT_USER\Software\caller\Capabilities\UrlAssociations]
"tel"="caller"
"callto"="caller"
"sip"="caller"

[HKEY_CURRENT_USER\Software\RegisteredApplications]
"caller"="SOFTWARE\\caller\\Capabilities"

[HKEY_CURRENT_USER\Software\Classes\caller]
@="Internet Call Protocol"

[HKEY_CURRENT_USER\Software\Classes\caller\DefaultIcon]
@="C:\\Program Files\\Fanvil\\caller.exe,0"

[HKEY_CURRENT_USER\Software\Classes\caller\shell]

[HKEY_CURRENT_USER\Software\Classes\caller\shell\open]

[HKEY_CURRENT_USER\Software\Classes\caller\shell\open\command]
@="\"C:\\Program Files\\Fanvil\\caller.exe\" \"%1\""

Исходя из указанных выше путей, можно понять, куда я разместил саму программу.

Рядом с программой должен лежать файл config.toml, в котором хранится URL для передачи номера. Его содержимое в моём случае выглядит примерно так:

# config.toml
url = "http://admin:пароль@192.168.0.10/cgi-bin/ConfigManApp.com?key="

Где 192.168.0.10 – это ip адрес телефона, ну а с паролем и логином и так всё понятно… В телефоне должна быть включена функция набора через URL.