diff --git a/contrib/vertobot/bot.pl b/contrib/vertobot/bot.pl
new file mode 100755
index 0000000000..828fc48786
--- /dev/null
+++ b/contrib/vertobot/bot.pl
@@ -0,0 +1,300 @@
+#!/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 = "";
+ 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},
+ });
+ $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";
+}
+
+my $verto_connecting = $loop->new_future;
+$bot_verto->connect(
+ %{ $CONFIG{"verto-bot"} },
+ on_connected => sub {
+ warn("[Verto] connected to websocket");
+ $verto_connecting->done($bot_verto) if not $verto_connecting->is_done;
+ },
+ on_connect_error => sub { die "Cannot connect to verto - $_[-1]" },
+ on_resolve_error => sub { die "Cannot resolve to verto - $_[-1]" },
+);
+
+Future->needs_all(
+ $bot_matrix->login( %{ $CONFIG{"matrix-bot"} } )->then( sub {
+ $bot_matrix->start;
+ }),
+
+ $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 = $@;
+
+# 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});
+ }
+ }
+}
+
|