From e72acb0cb34f9432d6e388c3f6a742d64277467c Mon Sep 17 00:00:00 2001 From: Daniele Pallastrelli <5451767+daniele77@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:23:50 +0100 Subject: [PATCH] Fix Channel recording would not stop. Fixes #60 --- CMakeLists.txt | 2 +- ChangeLog.md | 6 +- examples/CMakeLists.txt | 2 +- examples/Makefile | 2 +- examples/digitsequence.cpp | 397 +++++++++++++++++++++++++++++++++++ examples/makefile.win | 2 +- examples/play_and_record.cpp | 8 +- include/aricpp/channel.h | 6 +- 8 files changed, 412 insertions(+), 13 deletions(-) create mode 100644 examples/digitsequence.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 950e7cc..4e078bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ cmake_minimum_required(VERSION 3.8) -project(aricpp VERSION 1.1.1 LANGUAGES CXX) +project(aricpp VERSION 1.1.2 LANGUAGES CXX) include(GNUInstallDirs) diff --git a/ChangeLog.md b/ChangeLog.md index 074620a..26fb7ad 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,8 +1,10 @@ # Changelog -## Unversioned +## Version 1.1.2 - 2022-02-21 - - Fix passing two times the playback id in Channel::Play + - Fix Channel recording would not stop. Fixes [#47](https://github.com/daniele77/aricpp/issues/60) + - Fix passing two times the playback id in Channel::Play + - Add digitsequence new example ## Version 1.1.1 - 2021-05-18 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index eeb623b..f3c73f1 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -32,7 +32,7 @@ set(Boost_NO_BOOST_CMAKE ON) add_definitions(-DBOOST_ALL_DYN_LINK -DBOOST_UUID_FORCE_AUTO_LINK) # for windows: play_and_record uses boost uuid find_package(Boost 1.66 REQUIRED COMPONENTS program_options REQUIRED) -set(SOURCES low_level_dial high_level_dial chat query holding_bridge play_and_record auto_attendant) +set(SOURCES low_level_dial high_level_dial chat query holding_bridge play_and_record digitsequence auto_attendant) foreach(example ${SOURCES}) add_executable(${example} ${example}.cpp) diff --git a/examples/Makefile b/examples/Makefile index cfe426e..8aeb412 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -33,7 +33,7 @@ override CXXFLAGS += -O3 -Werror -Wall -Wextra -Wpedantic -std=c++1y -I.. override LDLIBS += -lboost_program_options -lboost_system -lpthread -EXAMPLES := low_level_dial high_level_dial chat query holding_bridge play_and_record auto_attendant +EXAMPLES := low_level_dial high_level_dial chat query holding_bridge play_and_record digitsequence auto_attendant .PHONY: clean all diff --git a/examples/digitsequence.cpp b/examples/digitsequence.cpp new file mode 100644 index 0000000..995a1fb --- /dev/null +++ b/examples/digitsequence.cpp @@ -0,0 +1,397 @@ +/******************************************************************************* + * ARICPP - ARI interface for C++ + * Copyright (C) 2017-2021 Daniele Pallastrelli + * + * This file is part of aricpp. + * For more information, see http://github.com/daniele77/aricpp + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../include/aricpp/arimodel.h" +#include "../include/aricpp/bridge.h" +#include "../include/aricpp/channel.h" +#include "../include/aricpp/client.h" + +using namespace aricpp; +using namespace std; + +#if BOOST_VERSION < 106600 + using IoContext = boost::asio::io_service; +#else + using IoContext = boost::asio::io_context; +#endif + +// +class SequenceDetector +{ +public: + SequenceDetector(boost::asio::io_context& ios, std::chrono::milliseconds _timeout, function&& _invalidSeqHandler) : + invalidSeqHandler(move(_invalidSeqHandler)), + timeout(_timeout), + timer(ios) + { + Reset(); + } + void AddSequence(string&& sequence, function&& handler) + { + maxSeqSize = max(maxSeqSize, sequence.size()); + sequences.emplace_back(move(sequence), move(handler)); + } + void Reset() + { + invalid = false; + inputSequence.clear(); + StopTimer(); + } + void StartDetection() + { + Reset(); + StartTimer(); + } + bool Symbol(char s) + { + if (invalid) return false; + + inputSequence += s; + for (auto& seq: sequences) + { + if (seq.first == inputSequence) + { + StopTimer(); + seq.second(inputSequence); + return true; + } + } + + // check whether the inputSequence is invalid + if (inputSequence.size() >= maxSeqSize) + Invalid(); + + return false; + } +private: + void StartTimer() + { + timer.expires_after(timeout); + timer.async_wait( + [this](boost::system::error_code e) + { + if (e) return; + TimerExpired(); + }); + } + void StopTimer() + { + timer.cancel(); + } + void Invalid() + { + invalid = true; + invalidSeqHandler(inputSequence); + } + void TimerExpired() + { + Invalid(); + } +private: + using Sequence = pair>; + vector sequences; + function invalidSeqHandler; + string inputSequence = {}; + size_t maxSeqSize = 0; + bool invalid = false; + const std::chrono::milliseconds timeout; + boost::asio::steady_timer timer; +}; + +// + +class Call +{ +public: + Call(IoContext& ios, shared_ptr callingCh) : + calling(std::move(callingCh)), + sequenceDetector(ios, 10s, [this](string pin){ InvalidPin(pin); }) + { + // here you can put all digit sequences and handlers + sequenceDetector.AddSequence("777#", [this](string digits) + { + cout << "got " << digits << endl; + calling->Hangup(); + }); + sequenceDetector.AddSequence("8888#", [this](string digits) + { + cout << "got " << digits << endl; + calling->StartMoh(); + }); + } + + bool HasChannel(const Channel& ch) const + { + return (calling->Id() == ch.Id()); + } + + void DialedChRinging() + { + // TODO + } + + void Digit(const string& digits) + { + for (char d: digits) + sequenceDetector.Symbol(d); + } + + void DialedChStart() + { + calling->Answer(); + } + + void PlaybackFinished() + { + sequenceDetector.StartDetection(); + } + + void DialingChUp() + { + calling->Play("sound:tt-monkeys"); + } + + bool ChHangup(const shared_ptr& /*hung*/) + { + // TODO + return true; + } + +private: + + void InvalidPin(const string& pin) + { + cout << "Wrong pin: " << pin << endl; + calling->Hangup(); + } + + shared_ptr calling; + unique_ptr bridge; + SequenceDetector sequenceDetector; +}; + +class CallContainer +{ +public: + CallContainer(IoContext& _ios, string app, AriModel& m, bool _sipCh) : + application(move(app)), + channels(m), + chPrefix(CalcChPrefix(_sipCh)), + ios(_ios) + { + channels.OnStasisStarted( + [this](shared_ptr ch, bool external) + { + if (external) + CallingChannel(ch); + else + cerr << "Internal channel not allowed" << endl; + }); + channels.OnChannelDestroyed( + [this](shared_ptr ch) + { + auto call = FindCallByChannel(ch); + if (call) + Remove(call); + else + cerr << "Call with a channel " << ch->Id() << " not found (hangup event)" << endl; + }); + channels.OnChannelStateChanged( + [this](shared_ptr ch) + { + auto state = ch->GetState(); + if (state == Channel::State::ringing) + { + auto call = FindCallByChannel(ch); + if (call) + call->DialedChRinging(); + else + cerr << "Call with dialed ch id " << ch->Id() << " not found (ringing event)\n"; + } + else if (state == Channel::State::up) + { + auto call = FindCallByChannel(ch); + if (call) call->DialingChUp(); + } + }); + channels.OnChannelDtmfReceived( + [&](std::shared_ptr channel, const std::string& digit) + { + auto call = FindCallByChannel(channel); + if (call) call->Digit(digit); + }); + channels.OnPlaybackFinished( + [this](Playback p) + { + auto channel = pb2ch[p.Id()]; + auto call = FindCallByChannel(channel); + call->PlaybackFinished(); + } + ); + + } + CallContainer(const CallContainer&) = delete; + CallContainer(CallContainer&&) = delete; + CallContainer& operator=(const CallContainer&) = delete; + CallContainer& operator=(CallContainer&&) = delete; + +private: + void CallingChannel(const shared_ptr& callingCh) + { + const string callingId = callingCh->Id(); + const string name = callingCh->Name(); + const string ext = callingCh->Extension(); + const string callerNum = callingCh->CallerNum(); + string callerName = callingCh->CallerName(); + if (callerName.empty()) callerName = callerNum; + + callingCh->GetVar("CALLERID(all)") + .OnError([](Error, const string& msg) { cerr << "Error retrieving variable CALLERID: " << msg << endl; }) + .After([](auto var) { cout << "CALLERID variable = " << var << endl; }); + + calls.emplace_back(make_shared(ios, callingCh)); + } + + void Remove(const shared_ptr& call) { calls.erase(remove(calls.begin(), calls.end(), call), calls.end()); } + + // return empty shared_ptr if not found + shared_ptr FindCallByChannel(const shared_ptr ch) const + { + auto c = find_if(calls.begin(), calls.end(), [&](auto call) { return call->HasChannel(*ch); }); + return (c == calls.end() ? shared_ptr() : *c); + } + + static string CalcChPrefix(bool sipCh) { return sipCh ? "sip/" : "pjsip/"; } + + const string application; + vector> calls; + AriModel& channels; + const string chPrefix; + unordered_map> pb2ch; + IoContext& ios; +}; + +static std::string to_string(bool b) { return (b ? "true" : "false"); } + +int main(int argc, char* argv[]) +{ + try + { + string host = "localhost"; + string port = "8088"; + string username = "asterisk"; + string password = "asterisk"; + string application = "attendant"; + bool sipCh = false; // default = pjsip channel + + namespace po = boost::program_options; + po::options_description desc("Allowed options"); + desc.add_options() + ("help,h", "produce help message") + ("version,V", "print version string") + + ("host,H", po::value(&host), ("ip address of the ARI server ["s + host + ']').c_str()) + ("port,P", po::value(&port), ("port of the ARI server ["s + port + "]").c_str()) + ("username,u", po::value(&username), ("username of the ARI account on the server ["s + username + "]").c_str()) + ("password,p", po::value(&password), ("password of the ARI account on the server ["s + password + "]").c_str()) + ("application,a", po::value(&application), ("stasis application to use ["s + application + "]").c_str()) + ("sip-channel,S", po::bool_switch(&sipCh), ("use old sip channel instead of pjsip channel ["s + to_string(sipCh) + "]").c_str()) + ; + + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + + if (vm.count("help")) + { + cout << desc << "\n"; + return 0; + } + + if (vm.count("version")) + { + cout << "This is digits demo application v. 1.0, part of aricpp library\n"; + return 0; + } + + IoContext ios; + + // Register to handle the signals that indicate when the server should exit. + // It is safe to register for the same signal multiple times in a program, + // provided all registration for the specified signal is made through Asio. + boost::asio::signal_set signals(ios); + signals.add(SIGINT); + signals.add(SIGTERM); +#if defined(SIGQUIT) + signals.add(SIGQUIT); +#endif // defined(SIGQUIT) + signals.async_wait( + [&ios](boost::system::error_code /*ec*/, int /*signo*/) + { + cout << "Cleanup and exit application...\n"; + ios.stop(); + }); + + Client client(ios, host, port, username, password, application); + AriModel channels(client); + CallContainer calls(ios, application, channels, sipCh); + + client.Connect( + [](boost::system::error_code e) + { + if (e) + { + cerr << "Connection error: " << e.message() << endl; + } + else + cout << "Connected" << endl; + }, + 10s // reconnection seconds + ); + ios.run(); + } + catch (const exception& e) + { + cerr << "Exception in app: " << e.what() << ". Aborting\n"; + return -1; + } + return 0; +} diff --git a/examples/makefile.win b/examples/makefile.win index 8c81751..e4addfc 100644 --- a/examples/makefile.win +++ b/examples/makefile.win @@ -31,7 +31,7 @@ ################################################################################ #define macros -EXE_NAMES = high_level_dial.exe low_level_dial.exe chat.exe query.exe holding_bridge.exe play_and_record.exe auto_attendant.exe +EXE_NAMES = high_level_dial.exe low_level_dial.exe chat.exe query.exe holding_bridge.exe play_and_record.exe digitsequence.exe auto_attendant.exe DIR_INCLUDE = /I..\.. /I%BOOST% DIR_LINK = %BOOST%\stage\lib COMPILE_FLAGS = /nologo /MD /EHsc /D_WIN32_WINNT=0x0501 /DBOOST_CONFIG_SUPPRESS_OUTDATED_MESSAGE diff --git a/examples/play_and_record.cpp b/examples/play_and_record.cpp index 33e7c48..58964b7 100644 --- a/examples/play_and_record.cpp +++ b/examples/play_and_record.cpp @@ -64,7 +64,7 @@ int main(int argc, char* argv[]) namespace po = boost::program_options; po::options_description desc("Allowed options"); - desc.add_options() + desc.add_options() ("help,h", "produce help message") ("version,V", "print version string") @@ -75,7 +75,7 @@ int main(int argc, char* argv[]) ("application,a", po::value(&application), ("stasis application to use ["s + application + "]").c_str()) ("sip-channel,S", po::bool_switch(&sipCh), ("use old sip channel instead of pjsip channel ["s + to_string(sipCh) + "]").c_str()) ; - + po::variables_map vm; po::store(po::parse_command_line(argc, argv, desc), vm); po::notify(vm); @@ -218,8 +218,8 @@ int main(int argc, char* argv[]) else cout << "Connected" << endl; }, - 10s /* reconnection seconds */); - + 10s // reconnection seconds + ); ios.run(); } catch (const exception& e) diff --git a/include/aricpp/channel.h b/include/aricpp/channel.h index 61dbf43..dfd5a2f 100644 --- a/include/aricpp/channel.h +++ b/include/aricpp/channel.h @@ -348,7 +348,7 @@ class Channel #endif // ARICPP_DEPRECATED_API ProxyPar Record( - const std::string& _name, + const std::string& recName, const std::string& format, const std::chrono::seconds& maxDuration = std::chrono::seconds::zero(), const std::chrono::seconds& maxSilence = std::chrono::seconds::zero(), @@ -357,11 +357,11 @@ class Channel const TerminationDtmf& terminateOn=TerminationDtmf::none ) const { - Recording recording(name, client); + Recording recording(recName, client); return ProxyPar::Command( Method::post, "/ari/channels/"+id+"/record?" - "name=" + UrlEncode(_name) + + "name=" + UrlEncode(recName) + "&format=" + format + "&terminateOn=" + static_cast(terminateOn) + ( beep ? "&beep=true" : "&beep=false" ) +