├── src ├── Parser.php ├── Commands │ ├── AmiUssd.php │ ├── OutputStyle.php │ ├── AmiListen.php │ ├── AmiCli.php │ ├── AmiAbstract.php │ ├── AmiSms.php │ ├── AmiAction.php │ └── Command.php ├── Factory.php └── Providers │ └── AmiServiceProvider.php ├── bin └── ami ├── tests ├── Factory.php ├── AmiServiceProvider.php ├── ActionTest.php ├── TestCase.php └── EventTest.php ├── LICENSE.md ├── composer.json └── config └── ami.php /src/Parser.php: -------------------------------------------------------------------------------- 1 | instance('config', new Repository()); 15 | (new EventServiceProvider($container))->register(); 16 | (new AmiServiceProvider($container))->register(); 17 | 18 | $app = new Application($container, $container['events'], null); 19 | $app->run(); 20 | -------------------------------------------------------------------------------- /tests/Factory.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 23 | } 24 | 25 | /** 26 | * Create client. 27 | * 28 | * @param array $options 29 | * 30 | * @return \React\Promise\Promise 31 | */ 32 | public function create(array $options = []) 33 | { 34 | return new FulfilledPromise(new Client($this->stream, new Parser())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Evgeni Razumov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Commands/AmiUssd.php: -------------------------------------------------------------------------------- 1 | call('ami:action', [ 31 | 'action' => 'DongleSendUSSD', 32 | '--arguments' => [ 33 | "Device:{$this->argument('device')}", 34 | "USSD:{$this->argument('ussd')}", 35 | ], 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/AmiServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerStream(); 19 | parent::register(); 20 | } 21 | 22 | /** 23 | * Register stream. 24 | */ 25 | protected function registerStream() 26 | { 27 | $this->app->singleton(Stream::class, function ($app) { 28 | return new Stream(fopen('php://memory', 'r+'), $app[LoopInterface::class]); 29 | }); 30 | $this->app->alias(Stream::class, 'ami.stream'); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function registerFactory() 37 | { 38 | $this->app->singleton(Factory::class, function ($app) { 39 | return new TestFactory($app[LoopInterface::class], $app[ConnectorInterface::class], $app[Stream::class]); 40 | }); 41 | $this->app->alias(Factory::class, 'ami.factory'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/ActionTest.php: -------------------------------------------------------------------------------- 1 | 'Success', 14 | 'ActionID' => '1', 15 | 'Message' => 'Channel status will follow', 16 | ], 17 | ]; 18 | $this->events->listen('ami.action.sended', function () use ($messages) { 19 | $this->assertTrue(true); 20 | $this->stream->emit('data', ["Asterisk Call Manager/1.3\r\n"]); 21 | foreach ($messages as $lines) { 22 | $message = ''; 23 | foreach ($lines as $key => $value) { 24 | $message .= "{$key}: {$value}\r\n"; 25 | } 26 | $this->stream->emit('data', ["{$message}\r\n"]); 27 | } 28 | }); 29 | $this->events->listen('ami.action.responsed', function ($console, $action, Response $response) { 30 | $this->assertEquals('Status', $action); 31 | $this->assertEquals($response->getFields(), [ 32 | 'Response' => 'Success', 33 | 'ActionID' => '1', 34 | 'Message' => 'Channel status will follow', 35 | ]); 36 | }); 37 | $this->console('ami:action', [ 38 | 'action' => 'Status', 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | instance('config', new Repository()); 41 | (new EventServiceProvider($app))->register(); 42 | (new AmiServiceProvider($app))->register(); 43 | $this->loop = $app[LoopInterface::class]; 44 | $this->loop->nextTick(function () { 45 | if (!$this->running) { 46 | $this->loop->stop(); 47 | } 48 | }); 49 | $this->stream = $app[Stream::class]; 50 | $this->events = $app['events']; 51 | $this->app = $app; 52 | } 53 | 54 | /** 55 | * Call console command. 56 | * 57 | * @param string $command 58 | * @param array $options 59 | */ 60 | protected function console($command, array $options = []) 61 | { 62 | return (new Console($this->app, $this->events, '5.3'))->call($command, $options); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Commands/OutputStyle.php: -------------------------------------------------------------------------------- 1 | output = $output; 27 | 28 | parent::__construct($input, $output); 29 | } 30 | 31 | /** 32 | * Returns whether verbosity is quiet (-q). 33 | * 34 | * @return bool 35 | */ 36 | public function isQuiet() 37 | { 38 | return $this->output->isQuiet(); 39 | } 40 | 41 | /** 42 | * Returns whether verbosity is verbose (-v). 43 | * 44 | * @return bool 45 | */ 46 | public function isVerbose() 47 | { 48 | return $this->output->isVerbose(); 49 | } 50 | 51 | /** 52 | * Returns whether verbosity is very verbose (-vv). 53 | * 54 | * @return bool 55 | */ 56 | public function isVeryVerbose() 57 | { 58 | return $this->output->isVeryVerbose(); 59 | } 60 | 61 | /** 62 | * Returns whether verbosity is debug (-vvv). 63 | * 64 | * @return bool 65 | */ 66 | public function isDebug() 67 | { 68 | return $this->output->isDebug(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enniel/ami", 3 | "type": "library", 4 | "description": "Provide asterisk ami to laravel", 5 | "keywords": [ 6 | "Enniel", 7 | "Ami", 8 | "Asterisk" 9 | ], 10 | "homepage": "https://github.com/Enniel/Ami", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Enniel" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=5.6.0", 19 | "ext-mbstring": "*", 20 | "illuminate/support": "~5.1", 21 | "illuminate/console": "~5.1", 22 | "illuminate/events": "~5.1", 23 | "illuminate/contracts": "~5.1", 24 | "react/dns": "~0.4.3", 25 | "react/stream": "~0.4.6", 26 | "react/event-loop": "~0.4.2", 27 | "react/socket-client": "~0.4.6", 28 | "clue/ami-react": "~0.3.1", 29 | "jackkum/phppdu": "~1.2.10" 30 | }, 31 | "require-dev": { 32 | "friendsofphp/php-cs-fixer": "~2.0.0", 33 | "phpunit/phpunit": "~4.5|~5.0", 34 | "illuminate/config": "~5.1", 35 | "illuminate/container": "~5.1" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Enniel\\Ami\\": "src" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Enniel\\Ami\\Tests\\": "tests" 45 | } 46 | }, 47 | "scripts": { 48 | "test": [ 49 | "@phpunit", 50 | "@phpcs" 51 | ], 52 | "phpunit": "php vendor/bin/phpunit", 53 | "phpcs": "php vendor/bin/php-cs-fixer --diff --dry-run -v fix" 54 | }, 55 | "bin": [ 56 | "bin/ami" 57 | ], 58 | "extra": { 59 | "branch-alias": { 60 | "dev-master": "2.0-dev" 61 | } 62 | }, 63 | "minimum-stability": "dev", 64 | "prefer-stable": true 65 | } 66 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | connector = $connector; 31 | $this->loop = $loop; 32 | } 33 | 34 | /** 35 | * Create client. 36 | * 37 | * @param array $options 38 | * 39 | * @return \React\Promise\Promise 40 | */ 41 | public function create(array $options = []) 42 | { 43 | foreach (['host', 'port', 'username', 'secret'] as $key) { 44 | $options[$key] = Arr::get($options, $key, null); 45 | } 46 | $promise = $this->connector->create($options['host'], $options['port'])->then(function (Stream $stream) { 47 | return new Client($stream, new Parser()); 48 | }); 49 | if (!is_null($options['username'])) { 50 | $promise = $promise->then(function (Client $client) use ($options) { 51 | $sender = new ActionSender($client); 52 | 53 | return $sender->login($options['username'], $options['secret'])->then( 54 | function () use ($client) { 55 | return $client; 56 | }, 57 | function ($error) use ($client) { 58 | $client->close(); 59 | throw $error; 60 | } 61 | ); 62 | }, function ($error) { 63 | throw $error; 64 | }); 65 | } 66 | 67 | return $promise; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Commands/AmiListen.php: -------------------------------------------------------------------------------- 1 | info($event->getName()); 39 | $fields = []; 40 | foreach ($event->getFields() as $key => $value) { 41 | $fields[] = [ 42 | $key, 43 | $value, 44 | ]; 45 | } 46 | $this->table($this->headers, $fields); 47 | } 48 | 49 | public function eventEmitter(Event $event) 50 | { 51 | $name = $event->getName(); 52 | $options = Arr::get($this->events, $name, []); 53 | $params = [$event, $options]; 54 | $this->dispatcher->fire('ami.events.*', $params); 55 | $this->dispatcher->fire('ami.events.'.$name, $params); 56 | } 57 | 58 | public function client(Client $client) 59 | { 60 | parent::client($client); 61 | $this->info('starting listen ami'); 62 | if ($this->option('monitor') && $this->runningInConsole()) { 63 | $client->on('event', [$this, 'eventMonitor']); 64 | } 65 | $client->on('close', function () { 66 | // the connection to the AMI just closed 67 | $this->info('closed listen ami'); 68 | }); 69 | $client->on('event', [$this, 'eventEmitter']); 70 | $this->dispatcher->fire('ami.listen.started', [$this, $client]); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Commands/AmiCli.php: -------------------------------------------------------------------------------- 1 | info('starting ami cli interface'); 40 | $command = $this->argument('cli'); 41 | if (!empty($command)) { 42 | $this->sendCommand($command); 43 | } else { 44 | $this->writeInterface(); 45 | } 46 | $client->on('close', function () { 47 | // the connection to the AMI just closed 48 | $this->info('closed ami cli'); 49 | }); 50 | } 51 | 52 | public function sendCommand($command) 53 | { 54 | $this->request('Command', [ 55 | 'Command' => $command, 56 | ])->then([$this, 'writeResponse'], [$this, 'writeException']); 57 | } 58 | 59 | public function writeInterface() 60 | { 61 | $command = $this->ask('Write command'); 62 | if (in_array(mb_strtolower($command), $this->exit)) { 63 | $this->stop(); 64 | } 65 | $this->sendCommand($command); 66 | } 67 | 68 | public function writeResponse(Response $response) 69 | { 70 | $this->line($response->getCommandOutput()); 71 | $autoclose = $this->option('autoclose'); 72 | if ($autoclose) { 73 | $this->stop(); 74 | } 75 | $this->writeInterface(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Commands/AmiAbstract.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 31 | $this->connector = $connector; 32 | $this->events = Arr::get($config, 'events', []); 33 | $this->config = $config; 34 | $this->dispatcher = $dispatcher; 35 | } 36 | 37 | /** 38 | * Execute the console command. 39 | * 40 | * @return mixed 41 | */ 42 | public function handle() 43 | { 44 | $options = $this->options(); 45 | foreach (['host', 'port', 'username', 'secret'] as $key) { 46 | $value = Arr::get($options, $key, null); 47 | $value = is_null($value) ? Arr::get($this->config, $key, null) : $value; 48 | $options[$key] = $value; 49 | } 50 | $client = $this->connector->create($options); 51 | $client->then([$this, 'client'], [$this, 'writeException']); 52 | $this->loop->run(); 53 | } 54 | 55 | public function client(Client $client) 56 | { 57 | $this->client = $client; 58 | $this->client->on('error', [$this, 'writeException']); 59 | } 60 | 61 | public function writeException(Exception $e) 62 | { 63 | $this->warn($e->getMessage()); 64 | $this->stop(); 65 | } 66 | 67 | public function writeResponse(Response $response) 68 | { 69 | $message = Arr::get($response->getFields(), 'Message', null); 70 | $this->line($message); 71 | $this->stop(); 72 | } 73 | 74 | public function request($action, array $options = []) 75 | { 76 | return $this->client->request($this->client->createAction($action, $options)); 77 | } 78 | 79 | public function stop() 80 | { 81 | $this->loop->stop(); 82 | 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Commands/AmiSms.php: -------------------------------------------------------------------------------- 1 | argument('device'); 39 | 40 | return $device ? $device : array_get($this->config, 'dongle.sms.device'); 41 | } 42 | 43 | public function sendSms() 44 | { 45 | $this->request('DongleSendSms', [ 46 | 'Device' => $this->getDevice(), 47 | 'Number' => $this->argument('number'), 48 | 'Message' => $this->argument('message'), 49 | ])->then([$this, 'writeResponse'], [$this, 'writeException']); 50 | } 51 | 52 | public function sendPdu() 53 | { 54 | $pdu = new Submit(); 55 | $pdu->setAddress($this->argument('number')); 56 | $pdu->setData($this->argument('message')); 57 | $promises = []; 58 | foreach ($pdu->getParts() as $part) { 59 | $promises[] = $this->request('DongleSendPdu', [ 60 | 'Device' => $this->getDevice(), 61 | 'PDU' => (string) $part, 62 | ]); 63 | } 64 | $promise = \React\Promise\map($promises, function (Response $response) { 65 | Event::fire('ami.dongle.sms.sended', [$this, $response]); 66 | $message = Arr::get($response->getFields(), 'Message', null); 67 | $this->line($message); 68 | }); 69 | $promise->then([$this, 'stop'], [$this, 'writeException']); 70 | } 71 | 72 | public function client(Client $client) 73 | { 74 | parent::client($client); 75 | $func = 'sendSms'; 76 | if ($this->option('pdu')) { 77 | $func = 'sendPdu'; 78 | } 79 | call_user_func([$this, $func]); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Commands/AmiAction.php: -------------------------------------------------------------------------------- 1 | option('arguments'); 43 | $arguments = is_array($arguments) ? $arguments : []; 44 | $options = []; 45 | $isAssoc = Arr::isAssoc($arguments); 46 | foreach ($arguments as $key => $value) { 47 | if (Str::contains($value, ':') && !$isAssoc) { 48 | $array = explode(':', $value); 49 | if ($key = Arr::get($array, 0)) { 50 | $value = Arr::get($array, 1, ''); 51 | $options[$key] = $value; 52 | } 53 | } else { 54 | $options[$key] = $value; 55 | } 56 | } 57 | 58 | $action = $this->argument('action'); 59 | $request = $this->request($action, $options); 60 | 61 | $this->dispatcher->fire('ami.action.sended', [$this, $action, $request]); 62 | 63 | $request->then( 64 | function (Response $response) use ($action) { 65 | $this->dispatcher->fire('ami.action.responsed', [$this, $action, $response]); 66 | if ($this->runningInConsole()) { 67 | $this->responseMonitor($response); 68 | } 69 | $this->stop(); 70 | }, 71 | function (Exception $exception) { 72 | throw $exception; 73 | }); 74 | } 75 | 76 | public function responseMonitor(Response $response) 77 | { 78 | $fields = []; 79 | foreach ($response->getFields() as $key => $value) { 80 | $fields[] = [ 81 | $key, 82 | $value, 83 | ]; 84 | } 85 | $this->table($this->headers, $fields); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /config/ami.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 5 | 'port' => 5038, 6 | 'username' => null, 7 | 'secret' => null, 8 | 'dongle' => [ 9 | 'sms' => [ 10 | 'device' => null, 11 | ], 12 | ], 13 | 'events' => [ 14 | 'AGIExec' => [ 15 | ], 16 | 'AgentConnect' => [ 17 | ], 18 | 'AgentComplete' => [ 19 | ], 20 | 'Agentlogin' => [ 21 | ], 22 | 'Agentlogoff' => [ 23 | ], 24 | 'Agents' => [ 25 | ], 26 | 'AsyncAGI' => [ 27 | ], 28 | 'Bridge' => [ 29 | ], 30 | 'CDR' => [ 31 | ], 32 | 'CEL' => [ 33 | ], 34 | 'ChannelUpdate' => [ 35 | ], 36 | 'CoreShowChannel' => [ 37 | ], 38 | 'CoreShowChannelsComplete' => [ 39 | ], 40 | 'DAHDIShowChannelsComplete' => [ 41 | ], 42 | 'DAHDIShowChannels' => [ 43 | ], 44 | 'DBGetResponse' => [ 45 | ], 46 | 'DTMF' => [ 47 | ], 48 | 'Dial' => [ 49 | ], 50 | 'DongleDeviceEntry' => [ 51 | ], 52 | 'DongleNewCUSD' => [ 53 | ], 54 | 'DongleNewUSSDBase64' => [ 55 | ], 56 | 'DongleNewUSSD' => [ 57 | ], 58 | 'DongleSMSStatus' => [ 59 | ], 60 | 'DongleShowDevicesComplete' => [ 61 | ], 62 | 'DongleStatus' => [ 63 | ], 64 | 'DongleUSSDStatus' => [ 65 | ], 66 | 'DonglePortFail' => [ 67 | ], 68 | 'ExtensionStatus' => [ 69 | ], 70 | 'FullyBooted' => [ 71 | ], 72 | 'Hangup' => [ 73 | ], 74 | 'Hold' => [ 75 | ], 76 | 'JabberEvent' => [ 77 | ], 78 | 'Join' => [ 79 | ], 80 | 'Leave' => [ 81 | ], 82 | 'Link' => [ 83 | ], 84 | 'ListDialPlan' => [ 85 | ], 86 | 'Masquerade' => [ 87 | ], 88 | 'MessageWaiting' => [ 89 | ], 90 | 'MusicOnHold' => [ 91 | ], 92 | 'NewAccountCode' => [ 93 | ], 94 | 'NewCallerid' => [ 95 | ], 96 | 'Newchannel' => [ 97 | ], 98 | 'Newexten' => [ 99 | ], 100 | 'Newstate' => [ 101 | ], 102 | 'OriginateResponse' => [ 103 | ], 104 | 'ParkedCall' => [ 105 | ], 106 | 'ParkedCallsComplete' => [ 107 | ], 108 | 'PeerEntry' => [ 109 | ], 110 | 'PeerStatus' => [ 111 | ], 112 | 'PeerlistComplete' => [ 113 | ], 114 | 'QueueMemberAdded' => [ 115 | ], 116 | 'QueueMember' => [ 117 | ], 118 | 'QueueMemberPaused' => [ 119 | ], 120 | 'QueueMemberRemoved' => [ 121 | ], 122 | 'QueueMemberStatus' => [ 123 | ], 124 | 'QueueParams' => [ 125 | ], 126 | 'QueueStatusComplete' => [ 127 | ], 128 | 'QueueSummaryComplete' => [ 129 | ], 130 | 'QueueSummary' => [ 131 | ], 132 | 'RTCPReceived' => [ 133 | ], 134 | 'RTCPReceiverStat' => [ 135 | ], 136 | 'RTCPSent' => [ 137 | ], 138 | 'RTPReceiverStat' => [ 139 | ], 140 | 'RTPSenderStat' => [ 141 | ], 142 | 'RegistrationsComplete' => [ 143 | ], 144 | 'Registry' => [ 145 | ], 146 | 'Rename' => [ 147 | ], 148 | 'ShowDialPlanComplete' => [ 149 | ], 150 | 'StatusComplete' => [ 151 | ], 152 | 'Status' => [ 153 | ], 154 | 'Transfer' => [ 155 | ], 156 | 'UnParkedCall' => [ 157 | ], 158 | 'Unlink' => [ 159 | ], 160 | 'UserEvent' => [ 161 | ], 162 | 'VarSet' => [ 163 | ], 164 | ], 165 | ]; 166 | -------------------------------------------------------------------------------- /src/Providers/AmiServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 26 | realpath(__DIR__.'/../../config/ami.php') => config_path('ami.php'), 27 | ], 'ami'); 28 | } 29 | 30 | /** 31 | * Register any package services. 32 | */ 33 | public function register() 34 | { 35 | $this->registerConfig(); 36 | $this->registerEventLoop(); 37 | $this->registerConnector(); 38 | $this->registerFactory(); 39 | $this->registerDongleUssd(); 40 | $this->registerAmiListen(); 41 | $this->registerAmiAction(); 42 | $this->registerDongleSms(); 43 | $this->registerAmiCli(); 44 | $this->commands([ 45 | 'command.ami.dongle.ussd', 46 | 'command.ami.dongle.sms', 47 | 'command.ami.listen', 48 | 'command.ami.action', 49 | 'command.ami.cli', 50 | ]); 51 | } 52 | 53 | /** 54 | * Register the configuration. 55 | */ 56 | protected function registerConfig() 57 | { 58 | $this->mergeConfigFrom(realpath(__DIR__.'/../../config/ami.php'), 'ami'); 59 | } 60 | 61 | /** 62 | * Register the ami listen command. 63 | */ 64 | protected function registerAmiListen() 65 | { 66 | $this->app->singleton(AmiListen::class, function ($app) { 67 | return new AmiListen($app['events'], $app['ami.eventloop'], $app['ami.factory'], $app['config']['ami']); 68 | }); 69 | $this->app->alias(AmiListen::class, 'command.ami.listen'); 70 | } 71 | 72 | /** 73 | * Register the ami listen command. 74 | */ 75 | protected function registerAmiCli() 76 | { 77 | $this->app->singleton(AmiCli::class, function ($app) { 78 | return new AmiCli($app['events'], $app['ami.eventloop'], $app['ami.factory'], $app['config']['ami']); 79 | }); 80 | $this->app->alias(AmiCli::class, 'command.ami.cli'); 81 | } 82 | 83 | /** 84 | * Register the ami action sender. 85 | */ 86 | protected function registerAmiAction() 87 | { 88 | $this->app->singleton(AmiAction::class, function ($app) { 89 | return new AmiAction($app['events'], $app['ami.eventloop'], $app['ami.factory'], $app['config']['ami']); 90 | }); 91 | $this->app->alias(AmiAction::class, 'command.ami.action'); 92 | } 93 | 94 | /** 95 | * Register the dongle sms. 96 | */ 97 | protected function registerDongleSms() 98 | { 99 | $this->app->singleton(AmiSms::class, function ($app) { 100 | return new AmiSms($app['events'], $app['ami.eventloop'], $app['ami.factory'], $app['config']['ami']); 101 | }); 102 | $this->app->alias(AmiSms::class, 'command.ami.dongle.sms'); 103 | } 104 | 105 | /** 106 | * Register the dongle ussd. 107 | */ 108 | protected function registerDongleUssd() 109 | { 110 | $this->app->singleton(AmiUssd::class, function ($app) { 111 | return new AmiUssd(); 112 | }); 113 | $this->app->alias(AmiUssd::class, 'command.ami.dongle.ussd'); 114 | } 115 | 116 | /** 117 | * Register event loop. 118 | */ 119 | protected function registerEventLoop() 120 | { 121 | $this->app->singleton(LoopInterface::class, function () { 122 | return new StreamSelectLoop(); 123 | }); 124 | $this->app->alias(LoopInterface::class, 'ami.eventloop'); 125 | } 126 | 127 | /** 128 | * Register connector. 129 | */ 130 | protected function registerConnector() 131 | { 132 | $this->app->singleton(ConnectorInterface::class, function ($app) { 133 | $loop = $app[LoopInterface::class]; 134 | 135 | return new Connector($loop, (new DnsResolver())->create('8.8.8.8', $loop)); 136 | }); 137 | $this->app->alias(ConnectorInterface::class, 'ami.connector'); 138 | } 139 | 140 | /** 141 | * Register factory. 142 | */ 143 | protected function registerFactory() 144 | { 145 | $this->app->singleton(Factory::class, function ($app) { 146 | return new Factory($app[LoopInterface::class], $app[ConnectorInterface::class]); 147 | }); 148 | $this->app->alias(Factory::class, 'ami.factory'); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/EventTest.php: -------------------------------------------------------------------------------- 1 | 'AgentConnect', 14 | 'Privilege' => 'agent,all', 15 | 'Queue' => 'taxi-operators', 16 | 'Uniqueid' => '1321511811.113', 17 | 'Channel' => 'SIP/100-00000072', 18 | 'Member' => 'SIP/100', 19 | 'MemberName' => 'SIP/100', 20 | 'Holdtime' => '10', 21 | 'BridgedChannel' => '1321511815.114', 22 | 'Ringtime' => '9', 23 | ], 24 | [ 25 | 'Event' => 'AgentComplete', 26 | 'Privilege' => 'agent,all', 27 | 'Queue' => 'taxi-operators', 28 | 'Uniqueid' => '1321511811.113', 29 | 'Channel' => 'SIP/100-00000072', 30 | 'Member' => 'SIP/100', 31 | 'MemberName' => 'SIP/100', 32 | 'HoldTime' => '10', 33 | 'TalkTime' => '7', 34 | 'Reason' => 'caller', 35 | ], 36 | [ 37 | 'Event' => 'Bridge', 38 | 'Privilege' => 'call,all', 39 | 'Bridgestate' => 'Link', 40 | 'Bridgetype' => 'core', 41 | 'Channel1' => 'SIP/mangotrunk-0000016c', 42 | 'Channel2' => 'SIP/261-0000016d', 43 | 'Uniqueid1' => '1324068645.605', 44 | 'Uniqueid2' => '1324068650.606', 45 | 'Callerid1' => '74997623634', 46 | 'Callerid2' => '261', 47 | ], 48 | [ 49 | 'Event' => 'Dial', 50 | 'Privilege' => 'call,all', 51 | 'Subevent' => 'Begin', 52 | 'Channel' => 'SIP/mangotrunk-0000016c', 53 | 'Destination' => 'SIP/261-0000016d', 54 | 'Calleridnum' => '74997623634', 55 | 'Calleridname' => '74997623634', 56 | 'Uniqueid' => '1324068645.605', 57 | 'Destuniqueid' => '1324068650.606', 58 | 'Dialstring' => '261', 59 | ], 60 | [ 61 | 'Event' => 'FullyBooted', 62 | 'Privilege' => 'system,all', 63 | 'Status' => 'Fully Booted', 64 | ], 65 | [ 66 | 'Event' => 'Join', 67 | 'Privilege' => 'call,all', 68 | 'Channel' => 'SIP/multifon-out-00000071', 69 | 'CallerIDNum' => '79265224173', 70 | 'CallerIDName' => 'unknown', 71 | 'ConnectedLineNum' => 'unknown', 72 | 'ConnectedLineName' => 'unknown', 73 | 'Queue' => 'taxi-operators', 74 | 'Position' => '1', 75 | 'Count' => '1', 76 | 'Uniqueid' => '1321511811.113', 77 | ], 78 | [ 79 | 'Event' => 'Link', 80 | 'Channel1' => 'SIP/101-3f3f', 81 | 'Channel2' => 'Zap/2-1', 82 | 'Uniqueid1' => '1094154427.10', 83 | 'Uniqueid2' => '1094154427.11', 84 | ], 85 | [ 86 | 'Event' => 'DonglePortFail', 87 | 'Privilege' => 'call,all', 88 | 'Device' => '/dev/ttyUSB8', 89 | 'Message' => 'Response Failed', 90 | ], 91 | ]; 92 | $this->events->listen('ami.listen.started', function () use ($messages) { 93 | $this->assertTrue(true); 94 | $this->stream->emit('data', ["Asterisk Call Manager/1.3\r\n"]); 95 | foreach ($messages as $lines) { 96 | $message = ''; 97 | foreach ($lines as $key => $value) { 98 | $message .= "{$key}: {$value}\r\n"; 99 | } 100 | $this->stream->emit('data', ["{$message}\r\n"]); 101 | } 102 | }); 103 | $this->events->listen('ami.events.AgentConnect', function (Event $event) { 104 | $this->assertEquals($event->getFields(), [ 105 | 'Event' => 'AgentConnect', 106 | 'Privilege' => 'agent,all', 107 | 'Queue' => 'taxi-operators', 108 | 'Uniqueid' => '1321511811.113', 109 | 'Channel' => 'SIP/100-00000072', 110 | 'Member' => 'SIP/100', 111 | 'MemberName' => 'SIP/100', 112 | 'Holdtime' => '10', 113 | 'BridgedChannel' => '1321511815.114', 114 | 'Ringtime' => '9', 115 | ]); 116 | $this->assertEquals($event->getName(), 'AgentConnect'); 117 | }); 118 | $this->events->listen('ami.events.AgentComplete', function (Event $event) { 119 | $this->assertEquals($event->getFields(), [ 120 | 'Event' => 'AgentComplete', 121 | 'Privilege' => 'agent,all', 122 | 'Queue' => 'taxi-operators', 123 | 'Uniqueid' => '1321511811.113', 124 | 'Channel' => 'SIP/100-00000072', 125 | 'Member' => 'SIP/100', 126 | 'MemberName' => 'SIP/100', 127 | 'HoldTime' => '10', 128 | 'TalkTime' => '7', 129 | 'Reason' => 'caller', 130 | ]); 131 | $this->assertEquals($event->getName(), 'AgentComplete'); 132 | }); 133 | $this->events->listen('ami.events.Bridge', function (Event $event) { 134 | $this->assertEquals($event->getFields(), [ 135 | 'Event' => 'Bridge', 136 | 'Privilege' => 'call,all', 137 | 'Bridgestate' => 'Link', 138 | 'Bridgetype' => 'core', 139 | 'Channel1' => 'SIP/mangotrunk-0000016c', 140 | 'Channel2' => 'SIP/261-0000016d', 141 | 'Uniqueid1' => '1324068645.605', 142 | 'Uniqueid2' => '1324068650.606', 143 | 'Callerid1' => '74997623634', 144 | 'Callerid2' => '261', 145 | ]); 146 | $this->assertEquals($event->getName(), 'Bridge'); 147 | }); 148 | $this->events->listen('ami.events.Dial', function (Event $event) { 149 | $this->assertEquals($event->getFields(), [ 150 | 'Event' => 'Dial', 151 | 'Privilege' => 'call,all', 152 | 'Subevent' => 'Begin', 153 | 'Channel' => 'SIP/mangotrunk-0000016c', 154 | 'Destination' => 'SIP/261-0000016d', 155 | 'Calleridnum' => '74997623634', 156 | 'Calleridname' => '74997623634', 157 | 'Uniqueid' => '1324068645.605', 158 | 'Destuniqueid' => '1324068650.606', 159 | 'Dialstring' => '261', 160 | ]); 161 | $this->assertEquals($event->getName(), 'Dial'); 162 | }); 163 | $this->events->listen('ami.events.FullyBooted', function (Event $event) { 164 | $this->assertEquals($event->getFields(), [ 165 | 'Event' => 'FullyBooted', 166 | 'Privilege' => 'system,all', 167 | 'Status' => 'Fully Booted', 168 | ]); 169 | $this->assertEquals($event->getName(), 'FullyBooted'); 170 | }); 171 | $this->events->listen('ami.events.Join', function (Event $event) { 172 | $this->assertEquals($event->getFields(), [ 173 | 'Event' => 'Join', 174 | 'Privilege' => 'call,all', 175 | 'Channel' => 'SIP/multifon-out-00000071', 176 | 'CallerIDNum' => '79265224173', 177 | 'CallerIDName' => 'unknown', 178 | 'ConnectedLineNum' => 'unknown', 179 | 'ConnectedLineName' => 'unknown', 180 | 'Queue' => 'taxi-operators', 181 | 'Position' => '1', 182 | 'Count' => '1', 183 | 'Uniqueid' => '1321511811.113', 184 | ]); 185 | $this->assertEquals($event->getName(), 'Join'); 186 | }); 187 | $this->events->listen('ami.events.Link', function (Event $event) { 188 | $this->assertEquals($event->getFields(), [ 189 | 'Event' => 'Link', 190 | 'Channel1' => 'SIP/101-3f3f', 191 | 'Channel2' => 'Zap/2-1', 192 | 'Uniqueid1' => '1094154427.10', 193 | 'Uniqueid2' => '1094154427.11', 194 | ]); 195 | $this->assertEquals($event->getName(), 'Link'); 196 | }); 197 | $this->events->listen('ami.events.DonglePortFail', function (Event $event) { 198 | $this->assertEquals($event->getFields(), [ 199 | 'Event' => 'DonglePortFail', 200 | 'Privilege' => 'call,all', 201 | 'Device' => '/dev/ttyUSB8', 202 | 'Message' => 'Response Failed', 203 | ]); 204 | $this->assertEquals($event->getName(), 'DonglePortFail'); 205 | $this->running = false; 206 | }); 207 | $this->running = true; 208 | $this->console('ami:listen'); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Commands/Command.php: -------------------------------------------------------------------------------- 1 | OutputInterface::VERBOSITY_VERBOSE, 75 | 'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE, 76 | 'vvv' => OutputInterface::VERBOSITY_DEBUG, 77 | 'quiet' => OutputInterface::VERBOSITY_QUIET, 78 | 'normal' => OutputInterface::VERBOSITY_NORMAL, 79 | ]; 80 | 81 | /** 82 | * Create a new console command instance. 83 | */ 84 | public function __construct() 85 | { 86 | // We will go ahead and set the name, description, and parameters on console 87 | // commands just to make things a little easier on the developer. This is 88 | // so they don't have to all be manually specified in the constructors. 89 | if (isset($this->signature)) { 90 | $this->configureUsingFluentDefinition(); 91 | } else { 92 | parent::__construct($this->name); 93 | } 94 | 95 | $this->setDescription($this->description); 96 | 97 | if (!isset($this->signature)) { 98 | $this->specifyParameters(); 99 | } 100 | } 101 | 102 | /** 103 | * Determine if we are running in the console. 104 | * 105 | * @return bool 106 | */ 107 | public function runningInConsole() 108 | { 109 | return php_sapi_name() == 'cli'; 110 | } 111 | 112 | /** 113 | * Configure the console command using a fluent definition. 114 | */ 115 | protected function configureUsingFluentDefinition() 116 | { 117 | list($name, $arguments, $options) = Parser::parse($this->signature); 118 | 119 | parent::__construct($name); 120 | 121 | foreach ($arguments as $argument) { 122 | $this->getDefinition()->addArgument($argument); 123 | } 124 | 125 | foreach ($options as $option) { 126 | $this->getDefinition()->addOption($option); 127 | } 128 | } 129 | 130 | /** 131 | * Specify the arguments and options on the command. 132 | */ 133 | protected function specifyParameters() 134 | { 135 | // We will loop through all of the arguments and options for the command and 136 | // set them all on the base command instance. This specifies what can get 137 | // passed into these commands as "parameters" to control the execution. 138 | foreach ($this->getArguments() as $arguments) { 139 | call_user_func_array([$this, 'addArgument'], $arguments); 140 | } 141 | 142 | foreach ($this->getOptions() as $options) { 143 | call_user_func_array([$this, 'addOption'], $options); 144 | } 145 | } 146 | 147 | /** 148 | * Run the console command. 149 | * 150 | * @param \Symfony\Component\Console\Input\InputInterface $input 151 | * @param \Symfony\Component\Console\Output\OutputInterface $output 152 | * 153 | * @return int 154 | */ 155 | public function run(InputInterface $input, OutputInterface $output) 156 | { 157 | $this->input = $input; 158 | 159 | $this->output = new OutputStyle($input, $output); 160 | 161 | return parent::run($input, $output); 162 | } 163 | 164 | /** 165 | * Execute the console command. 166 | * 167 | * @param \Symfony\Component\Console\Input\InputInterface $input 168 | * @param \Symfony\Component\Console\Output\OutputInterface $output 169 | * 170 | * @return mixed 171 | */ 172 | protected function execute(InputInterface $input, OutputInterface $output) 173 | { 174 | $method = method_exists($this, 'handle') ? 'handle' : 'fire'; 175 | 176 | return $this->$method(); 177 | } 178 | 179 | /** 180 | * Call another console command. 181 | * 182 | * @param string $command 183 | * @param array $arguments 184 | * 185 | * @return int 186 | */ 187 | public function call($command, array $arguments = []) 188 | { 189 | $instance = $this->getApplication()->find($command); 190 | 191 | $arguments['command'] = $command; 192 | 193 | return $instance->run(new ArrayInput($arguments), $this->output); 194 | } 195 | 196 | /** 197 | * Call another console command silently. 198 | * 199 | * @param string $command 200 | * @param array $arguments 201 | * 202 | * @return int 203 | */ 204 | public function callSilent($command, array $arguments = []) 205 | { 206 | $instance = $this->getApplication()->find($command); 207 | 208 | $arguments['command'] = $command; 209 | 210 | return $instance->run(new ArrayInput($arguments), new NullOutput()); 211 | } 212 | 213 | /** 214 | * Determine if the given argument is present. 215 | * 216 | * @param string|int $name 217 | * 218 | * @return bool 219 | */ 220 | public function hasArgument($name) 221 | { 222 | return $this->input->hasArgument($name); 223 | } 224 | 225 | /** 226 | * Get the value of a command argument. 227 | * 228 | * @param string $key 229 | * 230 | * @return string|array 231 | */ 232 | public function argument($key = null) 233 | { 234 | if (is_null($key)) { 235 | return $this->input->getArguments(); 236 | } 237 | 238 | return $this->input->getArgument($key); 239 | } 240 | 241 | /** 242 | * Get all of the arguments passed to the command. 243 | * 244 | * @return array 245 | */ 246 | public function arguments() 247 | { 248 | return $this->argument(); 249 | } 250 | 251 | /** 252 | * Determine if the given option is present. 253 | * 254 | * @param string $name 255 | * 256 | * @return bool 257 | */ 258 | public function hasOption($name) 259 | { 260 | return $this->input->hasOption($name); 261 | } 262 | 263 | /** 264 | * Get the value of a command option. 265 | * 266 | * @param string $key 267 | * 268 | * @return string|array 269 | */ 270 | public function option($key = null) 271 | { 272 | if (is_null($key)) { 273 | return $this->input->getOptions(); 274 | } 275 | 276 | return $this->input->getOption($key); 277 | } 278 | 279 | /** 280 | * Get all of the options passed to the command. 281 | * 282 | * @return array 283 | */ 284 | public function options() 285 | { 286 | return $this->option(); 287 | } 288 | 289 | /** 290 | * Confirm a question with the user. 291 | * 292 | * @param string $question 293 | * @param bool $default 294 | * 295 | * @return bool 296 | */ 297 | public function confirm($question, $default = false) 298 | { 299 | return $this->output->confirm($question, $default); 300 | } 301 | 302 | /** 303 | * Prompt the user for input. 304 | * 305 | * @param string $question 306 | * @param string $default 307 | * 308 | * @return string 309 | */ 310 | public function ask($question, $default = null) 311 | { 312 | return $this->output->ask($question, $default); 313 | } 314 | 315 | /** 316 | * Prompt the user for input with auto completion. 317 | * 318 | * @param string $question 319 | * @param array $choices 320 | * @param string $default 321 | * 322 | * @return string 323 | */ 324 | public function anticipate($question, array $choices, $default = null) 325 | { 326 | return $this->askWithCompletion($question, $choices, $default); 327 | } 328 | 329 | /** 330 | * Prompt the user for input with auto completion. 331 | * 332 | * @param string $question 333 | * @param array $choices 334 | * @param string $default 335 | * 336 | * @return string 337 | */ 338 | public function askWithCompletion($question, array $choices, $default = null) 339 | { 340 | $question = new Question($question, $default); 341 | 342 | $question->setAutocompleterValues($choices); 343 | 344 | return $this->output->askQuestion($question); 345 | } 346 | 347 | /** 348 | * Prompt the user for input but hide the answer from the console. 349 | * 350 | * @param string $question 351 | * @param bool $fallback 352 | * 353 | * @return string 354 | */ 355 | public function secret($question, $fallback = true) 356 | { 357 | $question = new Question($question); 358 | 359 | $question->setHidden(true)->setHiddenFallback($fallback); 360 | 361 | return $this->output->askQuestion($question); 362 | } 363 | 364 | /** 365 | * Give the user a single choice from an array of answers. 366 | * 367 | * @param string $question 368 | * @param array $choices 369 | * @param string $default 370 | * @param mixed $attempts 371 | * @param bool $multiple 372 | * 373 | * @return string 374 | */ 375 | public function choice($question, array $choices, $default = null, $attempts = null, $multiple = null) 376 | { 377 | $question = new ChoiceQuestion($question, $choices, $default); 378 | 379 | $question->setMaxAttempts($attempts)->setMultiselect($multiple); 380 | 381 | return $this->output->askQuestion($question); 382 | } 383 | 384 | /** 385 | * Format input to textual table. 386 | * 387 | * @param array $headers 388 | * @param \Illuminate\Contracts\Support\Arrayable|array $rows 389 | * @param string $style 390 | */ 391 | public function table(array $headers, $rows, $style = 'default') 392 | { 393 | $table = new Table($this->output); 394 | 395 | if ($rows instanceof Arrayable) { 396 | $rows = $rows->toArray(); 397 | } 398 | 399 | $table->setHeaders($headers)->setRows($rows)->setStyle($style)->render(); 400 | } 401 | 402 | /** 403 | * Write a string as information output. 404 | * 405 | * @param string $string 406 | * @param null|int|string $verbosity 407 | */ 408 | public function info($string, $verbosity = null) 409 | { 410 | $this->output->writeln("$string"); 411 | } 412 | 413 | /** 414 | * Write a string as standard output. 415 | * 416 | * @param string $string 417 | * @param string $style 418 | * @param null|int|string $verbosity 419 | */ 420 | public function line($string, $style = null, $verbosity = null) 421 | { 422 | $this->output->writeln($string); 423 | } 424 | 425 | /** 426 | * Write a string as comment output. 427 | * 428 | * @param string $string 429 | * @param null|int|string $verbosity 430 | */ 431 | public function comment($string, $verbosity = null) 432 | { 433 | $this->output->writeln("$string"); 434 | } 435 | 436 | /** 437 | * Write a string as question output. 438 | * 439 | * @param string $string 440 | * @param null|int|string $verbosity 441 | */ 442 | public function question($string, $verbosity = null) 443 | { 444 | $this->output->writeln("$string"); 445 | } 446 | 447 | /** 448 | * Write a string as error output. 449 | * 450 | * @param string $string 451 | * @param null|int|string $verbosity 452 | */ 453 | public function error($string, $verbosity = null) 454 | { 455 | $this->output->writeln("$string"); 456 | } 457 | 458 | /** 459 | * Write a string as warning output. 460 | * 461 | * @param string $string 462 | * @param null|int|string $verbosity 463 | */ 464 | public function warn($string, $verbosity = null) 465 | { 466 | if (!$this->output->getFormatter()->hasStyle('warning')) { 467 | $style = new OutputFormatterStyle('yellow'); 468 | 469 | $this->output->getFormatter()->setStyle('warning', $style); 470 | } 471 | 472 | $this->output->writeln("$string"); 473 | } 474 | 475 | /** 476 | * Get the verbosity level in terms of Symfony's OutputInterface level. 477 | * 478 | * @param string|int $level 479 | * 480 | * @return int 481 | */ 482 | protected function parseVerbosity($level = null) 483 | { 484 | if (isset($this->verbosityMap[$level])) { 485 | $level = $this->verbosityMap[$level]; 486 | } elseif (!is_int($level)) { 487 | $level = $this->verbosity; 488 | } 489 | 490 | return $level; 491 | } 492 | 493 | /** 494 | * Set the verbosity level. 495 | * 496 | * @param string|int $level 497 | */ 498 | protected function setVerbosity($level) 499 | { 500 | $this->verbosity = $this->parseVerbosity($level); 501 | } 502 | 503 | /** 504 | * Get the console command arguments. 505 | * 506 | * @return array 507 | */ 508 | protected function getArguments() 509 | { 510 | return []; 511 | } 512 | 513 | /** 514 | * Get the console command options. 515 | * 516 | * @return array 517 | */ 518 | protected function getOptions() 519 | { 520 | return []; 521 | } 522 | 523 | /** 524 | * Get the output implementation. 525 | * 526 | * @return \Symfony\Component\Console\Output\OutputInterface 527 | */ 528 | public function getOutput() 529 | { 530 | return $this->output; 531 | } 532 | 533 | /** 534 | * Get the Laravel application instance. 535 | * 536 | * @return \Illuminate\Contracts\Container\Container 537 | */ 538 | public function getLaravel() 539 | { 540 | return $this->laravel; 541 | } 542 | 543 | /** 544 | * Set the Laravel application instance. 545 | * 546 | * @param \Illuminate\Contracts\Container\Container $laravel 547 | */ 548 | public function setLaravel($laravel) 549 | { 550 | $this->laravel = $laravel; 551 | } 552 | } 553 | --------------------------------------------------------------------------------