summary refs log tree commit diff
path: root/contrib/vertobot
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/vertobot')
-rw-r--r--contrib/vertobot/.gitignore2
-rwxr-xr-xcontrib/vertobot/bot.pl300
-rw-r--r--contrib/vertobot/config.yaml29
-rw-r--r--contrib/vertobot/cpanfile17
-rw-r--r--contrib/vertobot/verto-example.json207
5 files changed, 555 insertions, 0 deletions
diff --git a/contrib/vertobot/.gitignore b/contrib/vertobot/.gitignore
new file mode 100644
index 0000000000..071a780574
--- /dev/null
+++ b/contrib/vertobot/.gitignore
@@ -0,0 +1,2 @@
+vucbot.yaml
+vertobot.yaml
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});
+        }
+    }
+}
+
diff --git a/contrib/vertobot/config.yaml b/contrib/vertobot/config.yaml
new file mode 100644
index 0000000000..04403670a9
--- /dev/null
+++ b/contrib/vertobot/config.yaml
@@ -0,0 +1,29 @@
+# Generic Matrix connection params
+matrix:
+  server: 'matrix.org'
+  SSL: 1
+
+# Bot-user connection details
+matrix-bot:
+  user_id: '@vertobot:matrix.org'
+  password: ''
+
+verto-bot:
+  host: webrtc.freeswitch.org
+  service: 8081
+  url: "ws://webrtc.freeswitch.org:8081/"
+
+verto-config:
+  passwd: 1234
+  
+verto-dialog-params:
+  useVideo: false
+  useStereo: false
+  tag: "webcam"
+  login: "1008@webrtc.freeswitch.org"
+  destination_number: "9664"
+  caller_id_name: "FreeSWITCH User"
+  caller_id_number: "1008"
+  callID: ""
+  remote_caller_id_name: "Outbound Call"
+  remote_caller_id_number: "9664"
diff --git a/contrib/vertobot/cpanfile b/contrib/vertobot/cpanfile
new file mode 100644
index 0000000000..c29fcaa6f6
--- /dev/null
+++ b/contrib/vertobot/cpanfile
@@ -0,0 +1,17 @@
+requires 'parent', 0;
+requires 'Future', '>= 0.29';
+requires 'Net::Async::Matrix', '>= 0.11_002';
+requires 'Net::Async::Matrix::Utils';
+requires 'Net::Async::WebSocket::Protocol', 0;
+requires 'Data::UUID', 0;
+requires 'IO::Async', '>= 0.63';
+requires 'IO::Async::SSL', 0;
+requires 'IO::Socket::SSL', 0;
+requires 'YAML', 0;
+requires 'JSON', 0;
+requires 'Getopt::Long', 0;
+
+on 'test' => sub {
+	requires 'Test::More', '>= 0.98';
+};
+
diff --git a/contrib/vertobot/verto-example.json b/contrib/vertobot/verto-example.json
new file mode 100644
index 0000000000..e0230498a6
--- /dev/null
+++ b/contrib/vertobot/verto-example.json
@@ -0,0 +1,207 @@
+# JSON is shown in *reverse* chronological order.
+# Send v. Receive is implicit.
+
+{
+    "jsonrpc": "2.0",
+    "id": 7,
+    "result": {
+        "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+        "message": "CALL ENDED",
+        "causeCode": 16,
+        "cause": "NORMAL_CLEARING",
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "method": "verto.bye",
+    "params": {
+        "dialogParams": {
+            "useVideo": false,
+            "useStereo": true,
+            "tag": "webcam",
+            "login": "1008@webrtc.freeswitch.org",
+            "destination_number": "9664",
+            "caller_id_name": "FreeSWITCH User",
+            "caller_id_number": "1008",
+            "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+            "remote_caller_id_name": "Outbound Call",
+            "remote_caller_id_number": "9664"
+        },
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    },
+    "id": 7
+}
+
+{
+    "jsonrpc": "2.0",
+    "id": 6,
+    "result": {
+        "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+        "action": "toggleHold",
+        "holdState": "active",
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "method": "verto.modify",
+    "params": {
+        "action": "toggleHold",
+        "dialogParams": {
+            "useVideo": false,
+            "useStereo": true,
+            "tag": "webcam",
+            "login": "1008@webrtc.freeswitch.org",
+            "destination_number": "9664",
+            "caller_id_name": "FreeSWITCH User",
+            "caller_id_number": "1008",
+            "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+            "remote_caller_id_name": "Outbound Call",
+            "remote_caller_id_number": "9664"
+        },
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    },
+    "id": 6
+}
+
+{
+    "jsonrpc": "2.0",
+    "id": 5,
+    "result": {
+        "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+        "action": "toggleHold",
+        "holdState": "held",
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "method": "verto.modify",
+    "params": {
+        "action": "toggleHold",
+        "dialogParams": {
+            "useVideo": false,
+            "useStereo": true,
+            "tag": "webcam",
+            "login": "1008@webrtc.freeswitch.org",
+            "destination_number": "9664",
+            "caller_id_name": "FreeSWITCH User",
+            "caller_id_number": "1008",
+            "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+            "remote_caller_id_name": "Outbound Call",
+            "remote_caller_id_number": "9664"
+        },
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    },
+    "id": 5
+}
+
+{
+    "jsonrpc": "2.0",
+    "id": 349819,
+    "result": {
+        "method": "verto.answer"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "id": 349819,
+    "method": "verto.answer",
+    "params": {
+        "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+        "sdp": "v=0\no=FreeSWITCH 1417101432 1417101433 IN IP4 209.105.235.10\ns=FreeSWITCH\nc=IN IP4 209.105.235.10\nt=0 0\na=msid-semantic: WMS jA3rmwLVwUq1iE6TYEYHeLk2YTUlh1Vq\nm=audio 30134 RTP/SAVPF 111 126\na=rtpmap:111 opus/48000/2\na=fmtp:111 minptime=10; stereo=1\na=rtpmap:126 telephone-event/8000\na=silenceSupp:off - - - -\na=ptime:20\na=sendrecv\na=fingerprint:sha-256 F8:72:18:E9:72:89:99:22:5B:F8:B6:C6:C6:0D:C5:9B:B2:FB:BC:CA:8D:AB:13:8A:66:E1:37:38:A0:16:AA:41\na=rtcp-mux\na=rtcp:30134 IN IP4 209.105.235.10\na=ssrc:210967934 cname:rOIEajpw4FocakWY\na=ssrc:210967934 msid:jA3rmwLVwUq1iE6TYEYHeLk2YTUlh1Vq a0\na=ssrc:210967934 mslabel:jA3rmwLVwUq1iE6TYEYHeLk2YTUlh1Vq\na=ssrc:210967934 label:jA3rmwLVwUq1iE6TYEYHeLk2YTUlh1Vqa0\na=ice-ufrag:OKwTmGLapwmxn7OF\na=ice-pwd:MmaMwq8rVmtWxfLbQ7U2Ew3T\na=candidate:2372654928 1 udp 659136 209.105.235.10 30134 typ host generation 0\n"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "id": 4,
+    "result": {
+        "message": "CALL CREATED",
+        "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "method": "verto.invite",
+    "params": {
+        "sdp": "v=0\r\no=- 1381685806032722557 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE audio\r\na=msid-semantic: WMS 6OOMyGAyJakjwaOOBtV7WcBCCuIW6PpuXsNg\r\nm=audio 63088 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 81.138.8.249\r\na=rtcp:63088 IN IP4 81.138.8.249\r\na=candidate:460398169 1 udp 2122260223 10.10.79.10 49945 typ host generation 0\r\na=candidate:460398169 2 udp 2122260223 10.10.79.10 49945 typ host generation 0\r\na=candidate:3460887983 1 udp 2122194687 192.168.1.64 63088 typ host generation 0\r\na=candidate:3460887983 2 udp 2122194687 192.168.1.64 63088 typ host generation 0\r\na=candidate:945327227 1 udp 1685987071 81.138.8.249 63088 typ srflx raddr 192.168.1.64 rport 63088 generation 0\r\na=candidate:945327227 2 udp 1685987071 81.138.8.249 63088 typ srflx raddr 192.168.1.64 rport 63088 generation 0\r\na=candidate:1441981097 1 tcp 1518280447 10.10.79.10 0 typ host tcptype active generation 0\r\na=candidate:1441981097 2 tcp 1518280447 10.10.79.10 0 typ host tcptype active generation 0\r\na=candidate:2160789855 1 tcp 1518214911 192.168.1.64 0 typ host tcptype active generation 0\r\na=candidate:2160789855 2 tcp 1518214911 192.168.1.64 0 typ host tcptype active generation 0\r\na=ice-ufrag:cP4qeRhn0LpcpA88\r\na=ice-pwd:fREmgSkXsDLGUUH1bwfrBQhW\r\na=ice-options:google-ice\r\na=fingerprint:sha-256 AF:35:64:1B:62:8A:EF:27:AE:2B:88:2E:FE:78:29:0B:08:DA:64:6C:DE:02:57:E3:EE:B1:D7:86:B8:36:8F:B0\r\na=setup:actpass\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10; stereo=1\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\na=ssrc:558827154 cname:vdKHBNqa17t2gmE3\r\na=ssrc:558827154 msid:6OOMyGAyJakjwaOOBtV7WcBCCuIW6PpuXsNg bf1303fb-9833-4d7d-b9e4-b32cfe04acc3\r\na=ssrc:558827154 mslabel:6OOMyGAyJakjwaOOBtV7WcBCCuIW6PpuXsNg\r\na=ssrc:558827154 label:bf1303fb-9833-4d7d-b9e4-b32cfe04acc3\r\n",
+        "dialogParams": {
+            "useVideo": false,
+            "useStereo": true,
+            "tag": "webcam",
+            "login": "1008@webrtc.freeswitch.org",
+            "destination_number": "9664",
+            "caller_id_name": "FreeSWITCH User",
+            "caller_id_number": "1008",
+            "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+            "remote_caller_id_name": "Outbound Call",
+            "remote_caller_id_number": "9664"
+        },
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    },
+    "id": 4
+}
+
+{
+    "jsonrpc": "2.0",
+    "id": 3,
+    "result": {
+        "message": "logged in",
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "id": 1,
+    "error": {
+        "code": -32000,
+        "message": "Authentication Required"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "method": "login",
+    "params": {
+        "login": "1008@webrtc.freeswitch.org",
+        "passwd": "1234",
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    },
+    "id": 3
+}
+
+{
+    "jsonrpc": "2.0",
+    "id": 2,
+    "error": {
+        "code": -32000,
+        "message": "Authentication Required"
+    }
+}
+
+{
+    "jsonrpc": "2.0",
+    "method": "login",
+    "params": {
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    },
+    "id": 1
+}
+
+{
+    "jsonrpc": "2.0",
+    "method": "login",
+    "params": {
+        "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+    },
+    "id": 2
+}