494 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
			
		
		
	
	
			494 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
| #!/usr/bin/env perl 
 | |
| 
 | |
| use strict;
 | |
| use warnings;
 | |
| use 5.010; # //
 | |
| use IO::Socket::SSL qw(SSL_VERIFY_NONE);
 | |
| use IO::Async::Loop;
 | |
| use Net::Async::WebSocket::Client;
 | |
| use Net::Async::HTTP;
 | |
| use Net::Async::HTTP::Server;
 | |
| use JSON;
 | |
| use YAML;
 | |
| use Data::UUID;
 | |
| use Getopt::Long;
 | |
| use Data::Dumper;
 | |
| use URI::Encode qw(uri_encode uri_decode);
 | |
|     
 | |
| binmode STDOUT, ":encoding(UTF-8)";
 | |
| binmode STDERR, ":encoding(UTF-8)";
 | |
| 
 | |
| my $msisdn_to_matrix = {
 | |
|     '447417892400' => '@matthew:matrix.org',
 | |
| };
 | |
| 
 | |
| my $matrix_to_msisdn = {};
 | |
| foreach (keys %$msisdn_to_matrix) {
 | |
|     $matrix_to_msisdn->{$msisdn_to_matrix->{$_}} = $_;
 | |
| }
 | |
| 
 | |
| 
 | |
| my $loop = IO::Async::Loop->new;
 | |
| # Net::Async::HTTP + SSL + IO::Poll doesn't play well. See
 | |
| #   https://rt.cpan.org/Ticket/Display.html?id=93107
 | |
| # ref $loop eq "IO::Async::Loop::Poll" and
 | |
| #     warn "Using SSL with IO::Poll causes known memory-leaks!!\n";
 | |
| 
 | |
| GetOptions(
 | |
|    'C|config=s' => \my $CONFIG,
 | |
|    'eval-from=s' => \my $EVAL_FROM,
 | |
| ) or exit 1;
 | |
| 
 | |
| if( defined $EVAL_FROM ) {
 | |
|     # An emergency 'eval() this file' hack
 | |
|     $SIG{HUP} = sub {
 | |
|         my $code = do {
 | |
|             open my $fh, "<", $EVAL_FROM or warn( "Cannot read - $!" ), return;
 | |
|             local $/; <$fh>
 | |
|         };
 | |
| 
 | |
|         eval $code or warn "Cannot eval() - $@";
 | |
|     };
 | |
| }
 | |
| 
 | |
| defined $CONFIG or die "Must supply --config\n";
 | |
| 
 | |
| my %CONFIG = %{ YAML::LoadFile( $CONFIG ) };
 | |
| 
 | |
| my %MATRIX_CONFIG = %{ $CONFIG{matrix} };
 | |
| # No harm in always applying this
 | |
| $MATRIX_CONFIG{SSL_verify_mode} = SSL_VERIFY_NONE;
 | |
| 
 | |
| my $bridgestate = {};
 | |
| my $roomid_by_callid = {};
 | |
|     
 | |
| my $sessid = lc new Data::UUID->create_str();    
 | |
| my $as_token = $CONFIG{"matrix-bot"}->{as_token};
 | |
| my $hs_domain = $CONFIG{"matrix-bot"}->{domain};
 | |
| 
 | |
| my $http = Net::Async::HTTP->new();
 | |
| $loop->add( $http );
 | |
| 
 | |
| sub create_virtual_user
 | |
| {
 | |
|     my ($localpart) = @_;
 | |
|     my ( $response ) = $http->do_request(
 | |
|         method => "POST",
 | |
|         uri => URI->new(
 | |
|             $CONFIG{"matrix"}->{server}.
 | |
|                 "/_matrix/client/api/v1/register?".
 | |
|                 "access_token=$as_token&user_id=$localpart"
 | |
|         ),
 | |
|         content_type => "application/json",
 | |
|         content => <<EOT
 | |
| {
 | |
|     "type": "m.login.application_service",
 | |
|     "user": "$localpart"
 | |
| }
 | |
| EOT
 | |
|     )->get;
 | |
|     warn $response->as_string if ($response->code != 200);
 | |
| }
 | |
|     
 | |
| my $http_server =  Net::Async::HTTP::Server->new(
 | |
|     on_request => sub {
 | |
|         my $self = shift;
 | |
|         my ( $req ) = @_;
 | |
| 
 | |
|         my $response;
 | |
|         my $path = uri_decode($req->path);
 | |
|         warn("request: $path");
 | |
|         if ($path =~ m#/users/\@(\+.*)#) {
 | |
|             # when queried about virtual users, auto-create them in the HS
 | |
|             my $localpart = $1;
 | |
|             create_virtual_user($localpart);
 | |
|             $response = HTTP::Response->new( 200 );
 | |
|             $response->add_content('{}');
 | |
|             $response->content_type( "application/json" );
 | |
|         }
 | |
|         elsif ($path =~ m#/transactions/(.*)#) {
 | |
|             my $event = JSON->new->decode($req->body);
 | |
|             print Dumper($event);
 | |
| 
 | |
|             my $room_id = $event->{room_id};
 | |
|             my %dp = %{$CONFIG{'verto-dialog-params'}};
 | |
|             $dp{callID} = $bridgestate->{$room_id}->{callid};
 | |
| 
 | |
|             if ($event->{type} eq 'm.room.membership') {
 | |
|                 my $membership = $event->{content}->{membership};
 | |
|                 my $state_key = $event->{state_key};
 | |
|                 my $room_id = $event->{state_id};
 | |
|                 
 | |
|                 if ($membership eq 'invite') {
 | |
|                     # autojoin invites
 | |
|                     my ( $response ) = $http->do_request(
 | |
|                         method => "POST",
 | |
|                         uri => URI->new(
 | |
|                             $CONFIG{"matrix"}->{server}.
 | |
|                                 "/_matrix/client/api/v1/rooms/$room_id/join?".
 | |
|                                 "access_token=$as_token&user_id=$state_key"
 | |
|                         ),
 | |
|                         content_type => "application/json",
 | |
|                         content => "{}",
 | |
|                     )->get;
 | |
|                     warn $response->as_string if ($response->code != 200);
 | |
|                 }
 | |
|             }
 | |
|             elsif ($event->{type} eq 'm.call.invite') {
 | |
|                 my $room_id = $event->{room_id};
 | |
|                 $bridgestate->{$room_id}->{matrix_callid} = $event->{content}->{call_id};
 | |
|                 $bridgestate->{$room_id}->{callid} = lc new Data::UUID->create_str();
 | |
|                 $bridgestate->{$room_id}->{sessid} = $sessid;                
 | |
|                 # $bridgestate->{$room_id}->{offer} = $event->{content}->{offer}->{sdp};
 | |
|                 my $offer = $event->{content}->{offer}->{sdp};
 | |
|                 # $bridgestate->{$room_id}->{gathered_candidates} = 0;
 | |
|                 $roomid_by_callid->{ $bridgestate->{$room_id}->{callid} } = $room_id;
 | |
|                 # no trickle ICE in verto apparently
 | |
| 
 | |
|                 my $f = send_verto_json_request("verto.invite", {
 | |
|                     "sdp" => $offer,
 | |
|                     "dialogParams" => \%dp,
 | |
|                     "sessid" => $bridgestate->{$room_id}->{sessid},
 | |
|                 });
 | |
|                 $self->adopt_future($f);
 | |
|             }
 | |
|             # elsif ($event->{type} eq 'm.call.candidates') {
 | |
|             #     # XXX: this could fire for both matrix->verto and verto->matrix calls
 | |
|             #     # and races as it collects candidates. much better to just turn off
 | |
|             #     # candidate gathering in the webclient entirely for now
 | |
|             #     
 | |
|             #     my $room_id = $event->{room_id};
 | |
|             #     # XXX: compare call IDs
 | |
|             #     if (!$bridgestate->{$room_id}->{gathered_candidates}) {
 | |
|             #         $bridgestate->{$room_id}->{gathered_candidates} = 1;
 | |
|             #         my $offer = $bridgestate->{$room_id}->{offer};
 | |
|             #         my $candidate_block = "";
 | |
|             #         foreach (@{$event->{content}->{candidates}}) {
 | |
|             #             $candidate_block .= "a=" . $_->{candidate} . "\r\n";
 | |
|             #         }
 | |
|             #         # XXX: collate using the right m= line - for now assume audio call
 | |
|             #         $offer =~ s/(a=rtcp.*[\r\n]+)/$1$candidate_block/;
 | |
|             #     
 | |
|             #         my $f = send_verto_json_request("verto.invite", {
 | |
|             #             "sdp" => $offer,
 | |
|             #             "dialogParams" => \%dp,
 | |
|             #             "sessid" => $bridgestate->{$room_id}->{sessid},
 | |
|             #         });
 | |
|             #         $self->adopt_future($f);
 | |
|             #     }
 | |
|             #     else {
 | |
|             #         # ignore them, as no trickle ICE, although we might as well
 | |
|             #         # batch them up
 | |
|             #         # foreach (@{$event->{content}->{candidates}}) {
 | |
|             #         #     push @{$bridgestate->{$room_id}->{candidates}}, $_;
 | |
|             #         # }
 | |
|             #     }
 | |
|             # }
 | |
|             elsif ($event->{type} eq 'm.call.answer') {
 | |
|                 # grab the answer and relay it to verto as a verto.answer
 | |
|                 my $room_id = $event->{room_id};
 | |
|                 
 | |
|                 my $answer = $event->{content}->{answer}->{sdp};
 | |
|                 my $f = send_verto_json_request("verto.answer", {
 | |
|                     "sdp" => $answer,
 | |
|                     "dialogParams" => \%dp,
 | |
|                     "sessid" => $bridgestate->{$room_id}->{sessid},
 | |
|                 });
 | |
|                 $self->adopt_future($f);
 | |
|             }
 | |
|             elsif ($event->{type} eq 'm.call.hangup') {
 | |
|                 my $room_id = $event->{room_id};
 | |
|                 if ($bridgestate->{$room_id}->{matrix_callid} eq $event->{content}->{call_id}) {
 | |
|                     my $f = send_verto_json_request("verto.bye", {
 | |
|                         "dialogParams" => \%dp,
 | |
|                         "sessid" => $bridgestate->{$room_id}->{sessid},
 | |
|                     });
 | |
|                     $self->adopt_future($f);
 | |
|                 }
 | |
|                 else {
 | |
|                     warn "Ignoring unrecognised callid: ".$event->{content}->{call_id};
 | |
|                 }
 | |
|             }
 | |
|             else {
 | |
|                 warn "Unhandled event: $event->{type}";
 | |
|             }
 | |
|             
 | |
|             $response = HTTP::Response->new( 200 );
 | |
|             $response->add_content('{}');
 | |
|             $response->content_type( "application/json" );            
 | |
|         }
 | |
|         else {
 | |
|             warn "Unhandled path: $path";
 | |
|             $response = HTTP::Response->new( 404 );
 | |
|         }
 | |
| 
 | |
|         $req->respond( $response );
 | |
|     },
 | |
| );
 | |
| $loop->add( $http_server );
 | |
| 
 | |
| $http_server->listen(
 | |
|     addr => { family => "inet", socktype => "stream", port => 8009 },
 | |
|     on_listen_error => sub { die "Cannot listen - $_[-1]\n" },
 | |
| );
 | |
| 
 | |
| my $bot_verto = Net::Async::WebSocket::Client->new(
 | |
|     on_frame => sub {
 | |
|           my ( $self, $frame ) = @_;
 | |
|           warn "[Verto] receiving $frame";
 | |
|           on_verto_json($frame);
 | |
|     },
 | |
| );
 | |
| $loop->add( $bot_verto );
 | |
| 
 | |
| my $verto_connecting = $loop->new_future;
 | |
| $bot_verto->connect(
 | |
|     %{ $CONFIG{"verto-bot"} },
 | |
|     on_connected => sub {
 | |
|         warn("[Verto] connected to websocket");
 | |
|         if (not $verto_connecting->is_done) {
 | |
|             $verto_connecting->done($bot_verto);
 | |
| 
 | |
|             send_verto_json_request("login", {
 | |
|                 'login' => $CONFIG{'verto-dialog-params'}{'login'},
 | |
|                 'passwd' => $CONFIG{'verto-config'}{'passwd'},
 | |
|                 'sessid' => $sessid,
 | |
|             });
 | |
|         }
 | |
|     },
 | |
|     on_connect_error => sub { die "Cannot connect to verto - $_[-1]" },
 | |
|     on_resolve_error => sub { die "Cannot resolve to verto - $_[-1]" },        
 | |
| );
 | |
| 
 | |
| # die Dumper($verto_connecting);
 | |
| 
 | |
| my $as_url = $CONFIG{"matrix-bot"}->{as_url};
 | |
| 
 | |
| Future->needs_all(
 | |
|     $http->do_request(
 | |
|             method => "POST",
 | |
|             uri => URI->new( $CONFIG{"matrix"}->{server}."/_matrix/appservice/v1/register" ),
 | |
|             content_type => "application/json",
 | |
|             content => <<EOT
 | |
| {
 | |
|     "as_token": "$as_token",
 | |
|     "url": "$as_url",
 | |
|     "namespaces": { "users": [ { "regex": "\@\\\\+.*", "exclusive": false } ] }
 | |
| }
 | |
| EOT
 | |
|     )->then( sub{
 | |
|         my ($response) = (@_);
 | |
|         warn $response->as_string if ($response->code != 200);
 | |
|         return Future->done;
 | |
|     }),
 | |
|     $verto_connecting,
 | |
| )->get;
 | |
| 
 | |
| $loop->attach_signal(
 | |
|     PIPE => sub { warn "pipe\n" }
 | |
| );
 | |
| $loop->attach_signal(
 | |
|     INT => sub { $loop->stop },
 | |
| );
 | |
| $loop->attach_signal(
 | |
|     TERM => sub { $loop->stop },
 | |
| );
 | |
| 
 | |
| eval {
 | |
|    $loop->run;
 | |
| } or my $e = $@;
 | |
| 
 | |
| die $e if $e;
 | |
| 
 | |
| exit 0;
 | |
| 
 | |
| {    
 | |
|     my $json_id;
 | |
|     my $requests;
 | |
| 
 | |
|     sub send_verto_json_request
 | |
|     {
 | |
|         $json_id ||= 1;
 | |
|         
 | |
|         my ($method, $params) = @_;
 | |
|         my $json = {
 | |
|             jsonrpc => "2.0",
 | |
|             method  => $method,
 | |
|             params  => $params,
 | |
|             id      => $json_id,
 | |
|         };
 | |
|         my $text = JSON->new->encode( $json );
 | |
|         warn "[Verto] sending $text";
 | |
|         $bot_verto->send_frame ( $text );
 | |
|         my $request = $loop->new_future;
 | |
|         $requests->{$json_id} = $request;
 | |
|         $json_id++;
 | |
|         return $request;
 | |
|     }
 | |
|     
 | |
|     sub send_verto_json_response
 | |
|     {
 | |
|         my ($result, $id) = @_;
 | |
|         my $json = {
 | |
|             jsonrpc => "2.0",
 | |
|             result  => $result,
 | |
|             id      => $id,
 | |
|         };
 | |
|         my $text = JSON->new->encode( $json );
 | |
|         warn "[Verto] sending $text";
 | |
|         $bot_verto->send_frame ( $text );
 | |
|     }
 | |
|     
 | |
|     sub on_verto_json
 | |
|     {
 | |
|         my $json = JSON->new->decode( $_[0] );
 | |
|         if ($json->{method}) {
 | |
|             if (($json->{method} eq 'verto.answer' && $json->{params}->{sdp}) ||
 | |
|                 $json->{method} eq 'verto.media') {
 | |
| 
 | |
|                 my $caller = $json->{dialogParams}->{caller_id_number};
 | |
|                 my $callee = $json->{dialogParams}->{destination_number};
 | |
|                 my $caller_user = '@+' . $caller . ':' . $hs_domain;
 | |
|                 my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";                                
 | |
|                 my $room_id = $roomid_by_callid->{$json->{params}->{callID}};
 | |
| 
 | |
|                 if ($json->{params}->{sdp}) {
 | |
|                     $http->do_request(
 | |
|                         method => "POST",
 | |
|                         uri => URI->new(
 | |
|                             $CONFIG{"matrix"}->{server}.
 | |
|                                 "/_matrix/client/api/v1/send/m.call.answer?".
 | |
|                                 "access_token=$as_token&user_id=$caller_user"
 | |
|                         ),
 | |
|                         content_type => "application/json",
 | |
|                         content => JSON->new->encode({
 | |
|                             call_id => $bridgestate->{$room_id}->{matrix_callid},
 | |
|                             version => 0,
 | |
|                             answer  => {
 | |
|                                 sdp => $json->{params}->{sdp},
 | |
|                                 type => "answer",
 | |
|                             },
 | |
|                         }),
 | |
|                     )->then( sub {
 | |
|                         send_verto_json_response( {
 | |
|                             method => $json->{method},
 | |
|                         }, $json->{id});
 | |
|                     })->get;
 | |
|                 }
 | |
|             }
 | |
|             elsif ($json->{method} eq 'verto.invite') {
 | |
|                 my $caller = $json->{dialogParams}->{caller_id_number};
 | |
|                 my $callee = $json->{dialogParams}->{destination_number};
 | |
|                 my $caller_user = '@+' . $caller . ':' . $hs_domain;
 | |
|                 my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";
 | |
|                     
 | |
|                 my $alias = ($caller lt $callee) ? ($caller.'-'.$callee) : ($callee.'-'.$caller);
 | |
|                 my $room_id;
 | |
| 
 | |
|                 # create a virtual user for the caller if needed.
 | |
|                 create_virtual_user($caller);
 | |
|                 
 | |
|                 # create a room of form #peer-peer and invite the callee
 | |
|                 $http->do_request(
 | |
|                     method => "POST",
 | |
|                     uri => URI->new(
 | |
|                         $CONFIG{"matrix"}->{server}.
 | |
|                             "/_matrix/client/api/v1/createRoom?".
 | |
|                             "access_token=$as_token&user_id=$caller_user"
 | |
|                     ),
 | |
|                     content_type => "application/json",
 | |
|                     content => JSON->new->encode({
 | |
|                         room_alias_name => $alias,
 | |
|                         invite => [ $callee_user ],
 | |
|                     }),
 | |
|                 )->then( sub {
 | |
|                     my ( $response ) = @_;
 | |
|                     my $resp = JSON->new->decode($response->content);
 | |
|                     $room_id = $resp->{room_id};
 | |
|                     $roomid_by_callid->{$json->{params}->{callID}} = $room_id;
 | |
|                 })->get;
 | |
| 
 | |
|                 # join it
 | |
|                 my ($response) = $http->do_request(
 | |
|                     method => "POST",
 | |
|                     uri => URI->new(
 | |
|                         $CONFIG{"matrix"}->{server}.
 | |
|                             "/_matrix/client/api/v1/join/$room_id?".
 | |
|                             "access_token=$as_token&user_id=$caller_user"
 | |
|                     ),
 | |
|                     content_type => "application/json",
 | |
|                     content => '{}',
 | |
|                 )->get;
 | |
| 
 | |
|                 $bridgestate->{$room_id}->{matrix_callid} = lc new Data::UUID->create_str();
 | |
|                 $bridgestate->{$room_id}->{callid} = $json->{dialogParams}->{callID};
 | |
|                 $bridgestate->{$room_id}->{sessid} = $sessid;
 | |
| 
 | |
|                 # put the m.call.invite in there
 | |
|                 $http->do_request(
 | |
|                     method => "POST",
 | |
|                     uri => URI->new(
 | |
|                         $CONFIG{"matrix"}->{server}.
 | |
|                             "/_matrix/client/api/v1/send/m.call.invite?".
 | |
|                             "access_token=$as_token&user_id=$caller_user"
 | |
|                     ),
 | |
|                     content_type => "application/json",
 | |
|                     content => JSON->new->encode({
 | |
|                         call_id => $bridgestate->{$room_id}->{matrix_callid},
 | |
|                         version => 0,
 | |
|                         answer  => {
 | |
|                             sdp => $json->{params}->{sdp},
 | |
|                             type => "offer",
 | |
|                         },
 | |
|                     }),
 | |
|                 )->then( sub {
 | |
|                     # acknowledge the verto
 | |
|                     send_verto_json_response( {
 | |
|                         method => $json->{method},
 | |
|                     }, $json->{id});
 | |
|                 })->get;
 | |
|             }
 | |
|             elsif ($json->{method} eq 'verto.bye') {
 | |
|                 my $caller = $json->{dialogParams}->{caller_id_number};
 | |
|                 my $callee = $json->{dialogParams}->{destination_number};
 | |
|                 my $caller_user = '@+' . $caller . ':' . $hs_domain;
 | |
|                 my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";                                
 | |
|                 my $room_id = $roomid_by_callid->{$json->{params}->{callID}};
 | |
|                 
 | |
|                 # put the m.call.hangup into the room
 | |
|                 $http->do_request(
 | |
|                     method => "POST",
 | |
|                     uri => URI->new(
 | |
|                         $CONFIG{"matrix"}->{server}.
 | |
|                             "/_matrix/client/api/v1/send/m.call.hangup?".
 | |
|                             "access_token=$as_token&user_id=$caller_user"
 | |
|                     ),
 | |
|                     content_type => "application/json",
 | |
|                     content => JSON->new->encode({
 | |
|                         call_id => $bridgestate->{$room_id}->{matrix_callid},
 | |
|                         version => 0,
 | |
|                     }),
 | |
|                 )->then( sub {
 | |
|                     # acknowledge the verto
 | |
|                     send_verto_json_response( {
 | |
|                         method => $json->{method},
 | |
|                     }, $json->{id});
 | |
|                 })->get;
 | |
|             }
 | |
|             else {
 | |
|                 warn ("[Verto] unhandled method: " . $json->{method});
 | |
|                 send_verto_json_response( {
 | |
|                     method => $json->{method},
 | |
|                 }, $json->{id});
 | |
|             }
 | |
|         }
 | |
|         elsif ($json->{result}) {
 | |
|             $requests->{$json->{id}}->done($json->{result});
 | |
|         }
 | |
|         elsif ($json->{error}) {
 | |
|             $requests->{$json->{id}}->fail($json->{error}->{message}, $json->{error});
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 |