123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- #!/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::Matrix 0.11_002;
- use JSON;
- use YAML;
- use Data::UUID;
- use Getopt::Long;
- use Data::Dumper;
- binmode STDOUT, ":encoding(UTF-8)";
- binmode STDERR, ":encoding(UTF-8)";
- 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;
- # Track every Room object, so we can ->leave them all on shutdown
- my %bot_matrix_rooms;
- my $bridgestate = {};
- my $roomid_by_callid = {};
- 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 $sessid = lc new Data::UUID->create_str();
- my $bot_matrix = Net::Async::Matrix->new(
- %MATRIX_CONFIG,
- on_log => sub { warn "log: @_\n" },
- on_invite => sub {
- my ($matrix, $invite) = @_;
- warn "[Matrix] invited to: " . $invite->{room_id} . " by " . $invite->{inviter} . "\n";
-
- $matrix->join_room( $invite->{room_id} )->get;
- },
- on_room_new => sub {
- my ($matrix, $room) = @_;
- warn "[Matrix] have a room ID: " . $room->room_id . "\n";
- $bot_matrix_rooms{$room->room_id} = $room;
-
- # log in to verto on behalf of this room
- $bridgestate->{$room->room_id}->{sessid} = $sessid;
-
- $room->configure(
- on_message => \&on_room_message,
- );
-
- my $f = send_verto_json_request("login", {
- 'login' => $CONFIG{'verto-dialog-params'}{'login'},
- 'passwd' => $CONFIG{'verto-config'}{'passwd'},
- 'sessid' => $sessid,
- });
- $matrix->adopt_future($f);
-
- # we deliberately don't paginate the room, as we only care about
- # new calls
- },
- on_unknown_event => \&on_unknown_event,
- on_error => sub {
- print STDERR "Matrix failure: @_\n";
- },
- );
- $loop->add( $bot_matrix );
- sub on_unknown_event
- {
- my ($matrix, $event) = @_;
- 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.call.invite') {
- $bridgestate->{$room_id}->{matrix_callid} = $event->{content}->{call_id};
- $bridgestate->{$room_id}->{callid} = lc new Data::UUID->create_str();
- $bridgestate->{$room_id}->{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
- }
- elsif ($event->{type} eq 'm.call.candidates') {
- # 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 = {
- audio => '',
- video => '',
- };
- foreach (@{$event->{content}->{candidates}}) {
- if ($_->{sdpMid}) {
- $candidate_block->{$_->{sdpMid}} .= "a=" . $_->{candidate} . "\r\n";
- }
- else {
- $candidate_block->{audio} .= "a=" . $_->{candidate} . "\r\n";
- $candidate_block->{video} .= "a=" . $_->{candidate} . "\r\n";
- }
- }
- # XXX: assumes audio comes first
- #$offer =~ s/(a=rtcp-mux[\r\n]+)/$1$candidate_block->{audio}/;
- #$offer =~ s/(a=rtcp-mux[\r\n]+)/$1$candidate_block->{video}/;
- $offer =~ s/(m=video)/$candidate_block->{audio}$1/;
- $offer =~ s/(.$)/$1\n$candidate_block->{video}$1/;
-
- my $f = send_verto_json_request("verto.invite", {
- "sdp" => $offer,
- "dialogParams" => \%dp,
- "sessid" => $bridgestate->{$room_id}->{sessid},
- });
- $matrix->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.hangup') {
- 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},
- });
- $matrix->adopt_future($f);
- }
- else {
- warn "Ignoring unrecognised callid: ".$event->{content}->{call_id};
- }
- }
- else {
- warn "Unhandled event: $event->{type}";
- }
- }
- sub on_room_message
- {
- my ($room, $from, $content) = @_;
- my $room_id = $room->room_id;
- warn "[Matrix] in $room_id: $from: " . $content->{body} . "\n";
- }
- Future->needs_all(
- $bot_matrix->login( %{ $CONFIG{"matrix-bot"} } )->then( sub {
- $bot_matrix->start;
- }),
-
- $bot_verto->connect(
- %{ $CONFIG{"verto-bot"} },
- on_connect_error => sub { die "Cannot connect to verto - $_[-1]" },
- on_resolve_error => sub { die "Cannot resolve to verto - $_[-1]" },
- )->on_done( sub {
- warn("[Verto] connected to websocket");
- }),
- )->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 = $@;
- # When the bot gets shut down, have it leave the rooms so it's clear to observers
- # that it is no longer running.
- # if( $CONFIG{"leave-on-shutdown"} // 1 ) {
- # print STDERR "Removing bot from Matrix rooms...\n";
- # Future->wait_all( map { $_->leave->else_done() } values %bot_matrix_rooms )->get;
- # }
- # else {
- # print STDERR "Leaving bot users in Matrix rooms.\n";
- # }
- 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 $room_id = $roomid_by_callid->{$json->{params}->{callID}};
- my $room = $bot_matrix_rooms{$room_id};
- if ($json->{params}->{sdp}) {
- # HACK HACK HACK HACK
- $room->_do_POST_json( "/send/m.call.answer", {
- 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;
- }
- }
- 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});
- }
- }
- }
|