From 84a4367657f5c97849f629bebb1e411532042290 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 4 Mar 2015 01:27:47 +0100 Subject: [PATCH] WIP vertobridge AS --- contrib/vertobot/bridge.pl | 489 +++++++++++++++++++++++++++++++++++++ 1 file changed, 489 insertions(+) create mode 100755 contrib/vertobot/bridge.pl diff --git a/contrib/vertobot/bridge.pl b/contrib/vertobot/bridge.pl new file mode 100755 index 000000000..e1a07f665 --- /dev/null +++ b/contrib/vertobot/bridge.pl @@ -0,0 +1,489 @@ +#!/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 => <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 => <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}); + } + } +} +