bridge.pl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. #!/usr/bin/env perl
  2. use strict;
  3. use warnings;
  4. use 5.010; # //
  5. use IO::Socket::SSL qw(SSL_VERIFY_NONE);
  6. use IO::Async::Loop;
  7. use Net::Async::WebSocket::Client;
  8. use Net::Async::HTTP;
  9. use Net::Async::HTTP::Server;
  10. use JSON;
  11. use YAML;
  12. use Data::UUID;
  13. use Getopt::Long;
  14. use Data::Dumper;
  15. use URI::Encode qw(uri_encode uri_decode);
  16. binmode STDOUT, ":encoding(UTF-8)";
  17. binmode STDERR, ":encoding(UTF-8)";
  18. my $msisdn_to_matrix = {
  19. '447417892400' => '@matthew:matrix.org',
  20. };
  21. my $matrix_to_msisdn = {};
  22. foreach (keys %$msisdn_to_matrix) {
  23. $matrix_to_msisdn->{$msisdn_to_matrix->{$_}} = $_;
  24. }
  25. my $loop = IO::Async::Loop->new;
  26. # Net::Async::HTTP + SSL + IO::Poll doesn't play well. See
  27. # https://rt.cpan.org/Ticket/Display.html?id=93107
  28. # ref $loop eq "IO::Async::Loop::Poll" and
  29. # warn "Using SSL with IO::Poll causes known memory-leaks!!\n";
  30. GetOptions(
  31. 'C|config=s' => \my $CONFIG,
  32. 'eval-from=s' => \my $EVAL_FROM,
  33. ) or exit 1;
  34. if( defined $EVAL_FROM ) {
  35. # An emergency 'eval() this file' hack
  36. $SIG{HUP} = sub {
  37. my $code = do {
  38. open my $fh, "<", $EVAL_FROM or warn( "Cannot read - $!" ), return;
  39. local $/; <$fh>
  40. };
  41. eval $code or warn "Cannot eval() - $@";
  42. };
  43. }
  44. defined $CONFIG or die "Must supply --config\n";
  45. my %CONFIG = %{ YAML::LoadFile( $CONFIG ) };
  46. my %MATRIX_CONFIG = %{ $CONFIG{matrix} };
  47. # No harm in always applying this
  48. $MATRIX_CONFIG{SSL_verify_mode} = SSL_VERIFY_NONE;
  49. my $bridgestate = {};
  50. my $roomid_by_callid = {};
  51. my $sessid = lc new Data::UUID->create_str();
  52. my $as_token = $CONFIG{"matrix-bot"}->{as_token};
  53. my $hs_domain = $CONFIG{"matrix-bot"}->{domain};
  54. my $http = Net::Async::HTTP->new();
  55. $loop->add( $http );
  56. sub create_virtual_user
  57. {
  58. my ($localpart) = @_;
  59. my ( $response ) = $http->do_request(
  60. method => "POST",
  61. uri => URI->new(
  62. $CONFIG{"matrix"}->{server}.
  63. "/_matrix/client/api/v1/register?".
  64. "access_token=$as_token&user_id=$localpart"
  65. ),
  66. content_type => "application/json",
  67. content => <<EOT
  68. {
  69. "type": "m.login.application_service",
  70. "user": "$localpart"
  71. }
  72. EOT
  73. )->get;
  74. warn $response->as_string if ($response->code != 200);
  75. }
  76. my $http_server = Net::Async::HTTP::Server->new(
  77. on_request => sub {
  78. my $self = shift;
  79. my ( $req ) = @_;
  80. my $response;
  81. my $path = uri_decode($req->path);
  82. warn("request: $path");
  83. if ($path =~ m#/users/\@(\+.*)#) {
  84. # when queried about virtual users, auto-create them in the HS
  85. my $localpart = $1;
  86. create_virtual_user($localpart);
  87. $response = HTTP::Response->new( 200 );
  88. $response->add_content('{}');
  89. $response->content_type( "application/json" );
  90. }
  91. elsif ($path =~ m#/transactions/(.*)#) {
  92. my $event = JSON->new->decode($req->body);
  93. print Dumper($event);
  94. my $room_id = $event->{room_id};
  95. my %dp = %{$CONFIG{'verto-dialog-params'}};
  96. $dp{callID} = $bridgestate->{$room_id}->{callid};
  97. if ($event->{type} eq 'm.room.membership') {
  98. my $membership = $event->{content}->{membership};
  99. my $state_key = $event->{state_key};
  100. my $room_id = $event->{state_id};
  101. if ($membership eq 'invite') {
  102. # autojoin invites
  103. my ( $response ) = $http->do_request(
  104. method => "POST",
  105. uri => URI->new(
  106. $CONFIG{"matrix"}->{server}.
  107. "/_matrix/client/api/v1/rooms/$room_id/join?".
  108. "access_token=$as_token&user_id=$state_key"
  109. ),
  110. content_type => "application/json",
  111. content => "{}",
  112. )->get;
  113. warn $response->as_string if ($response->code != 200);
  114. }
  115. }
  116. elsif ($event->{type} eq 'm.call.invite') {
  117. my $room_id = $event->{room_id};
  118. $bridgestate->{$room_id}->{matrix_callid} = $event->{content}->{call_id};
  119. $bridgestate->{$room_id}->{callid} = lc new Data::UUID->create_str();
  120. $bridgestate->{$room_id}->{sessid} = $sessid;
  121. # $bridgestate->{$room_id}->{offer} = $event->{content}->{offer}->{sdp};
  122. my $offer = $event->{content}->{offer}->{sdp};
  123. # $bridgestate->{$room_id}->{gathered_candidates} = 0;
  124. $roomid_by_callid->{ $bridgestate->{$room_id}->{callid} } = $room_id;
  125. # no trickle ICE in verto apparently
  126. my $f = send_verto_json_request("verto.invite", {
  127. "sdp" => $offer,
  128. "dialogParams" => \%dp,
  129. "sessid" => $bridgestate->{$room_id}->{sessid},
  130. });
  131. $self->adopt_future($f);
  132. }
  133. # elsif ($event->{type} eq 'm.call.candidates') {
  134. # # XXX: this could fire for both matrix->verto and verto->matrix calls
  135. # # and races as it collects candidates. much better to just turn off
  136. # # candidate gathering in the webclient entirely for now
  137. #
  138. # my $room_id = $event->{room_id};
  139. # # XXX: compare call IDs
  140. # if (!$bridgestate->{$room_id}->{gathered_candidates}) {
  141. # $bridgestate->{$room_id}->{gathered_candidates} = 1;
  142. # my $offer = $bridgestate->{$room_id}->{offer};
  143. # my $candidate_block = "";
  144. # foreach (@{$event->{content}->{candidates}}) {
  145. # $candidate_block .= "a=" . $_->{candidate} . "\r\n";
  146. # }
  147. # # XXX: collate using the right m= line - for now assume audio call
  148. # $offer =~ s/(a=rtcp.*[\r\n]+)/$1$candidate_block/;
  149. #
  150. # my $f = send_verto_json_request("verto.invite", {
  151. # "sdp" => $offer,
  152. # "dialogParams" => \%dp,
  153. # "sessid" => $bridgestate->{$room_id}->{sessid},
  154. # });
  155. # $self->adopt_future($f);
  156. # }
  157. # else {
  158. # # ignore them, as no trickle ICE, although we might as well
  159. # # batch them up
  160. # # foreach (@{$event->{content}->{candidates}}) {
  161. # # push @{$bridgestate->{$room_id}->{candidates}}, $_;
  162. # # }
  163. # }
  164. # }
  165. elsif ($event->{type} eq 'm.call.answer') {
  166. # grab the answer and relay it to verto as a verto.answer
  167. my $room_id = $event->{room_id};
  168. my $answer = $event->{content}->{answer}->{sdp};
  169. my $f = send_verto_json_request("verto.answer", {
  170. "sdp" => $answer,
  171. "dialogParams" => \%dp,
  172. "sessid" => $bridgestate->{$room_id}->{sessid},
  173. });
  174. $self->adopt_future($f);
  175. }
  176. elsif ($event->{type} eq 'm.call.hangup') {
  177. my $room_id = $event->{room_id};
  178. if ($bridgestate->{$room_id}->{matrix_callid} eq $event->{content}->{call_id}) {
  179. my $f = send_verto_json_request("verto.bye", {
  180. "dialogParams" => \%dp,
  181. "sessid" => $bridgestate->{$room_id}->{sessid},
  182. });
  183. $self->adopt_future($f);
  184. }
  185. else {
  186. warn "Ignoring unrecognised callid: ".$event->{content}->{call_id};
  187. }
  188. }
  189. else {
  190. warn "Unhandled event: $event->{type}";
  191. }
  192. $response = HTTP::Response->new( 200 );
  193. $response->add_content('{}');
  194. $response->content_type( "application/json" );
  195. }
  196. else {
  197. warn "Unhandled path: $path";
  198. $response = HTTP::Response->new( 404 );
  199. }
  200. $req->respond( $response );
  201. },
  202. );
  203. $loop->add( $http_server );
  204. $http_server->listen(
  205. addr => { family => "inet", socktype => "stream", port => 8009 },
  206. on_listen_error => sub { die "Cannot listen - $_[-1]\n" },
  207. );
  208. my $bot_verto = Net::Async::WebSocket::Client->new(
  209. on_frame => sub {
  210. my ( $self, $frame ) = @_;
  211. warn "[Verto] receiving $frame";
  212. on_verto_json($frame);
  213. },
  214. );
  215. $loop->add( $bot_verto );
  216. my $verto_connecting = $loop->new_future;
  217. $bot_verto->connect(
  218. %{ $CONFIG{"verto-bot"} },
  219. on_connected => sub {
  220. warn("[Verto] connected to websocket");
  221. if (not $verto_connecting->is_done) {
  222. $verto_connecting->done($bot_verto);
  223. send_verto_json_request("login", {
  224. 'login' => $CONFIG{'verto-dialog-params'}{'login'},
  225. 'passwd' => $CONFIG{'verto-config'}{'passwd'},
  226. 'sessid' => $sessid,
  227. });
  228. }
  229. },
  230. on_connect_error => sub { die "Cannot connect to verto - $_[-1]" },
  231. on_resolve_error => sub { die "Cannot resolve to verto - $_[-1]" },
  232. );
  233. # die Dumper($verto_connecting);
  234. my $as_url = $CONFIG{"matrix-bot"}->{as_url};
  235. Future->needs_all(
  236. $http->do_request(
  237. method => "POST",
  238. uri => URI->new( $CONFIG{"matrix"}->{server}."/_matrix/appservice/v1/register" ),
  239. content_type => "application/json",
  240. content => <<EOT
  241. {
  242. "as_token": "$as_token",
  243. "url": "$as_url",
  244. "namespaces": { "users": [ { "regex": "\@\\\\+.*", "exclusive": false } ] }
  245. }
  246. EOT
  247. )->then( sub{
  248. my ($response) = (@_);
  249. warn $response->as_string if ($response->code != 200);
  250. return Future->done;
  251. }),
  252. $verto_connecting,
  253. )->get;
  254. $loop->attach_signal(
  255. PIPE => sub { warn "pipe\n" }
  256. );
  257. $loop->attach_signal(
  258. INT => sub { $loop->stop },
  259. );
  260. $loop->attach_signal(
  261. TERM => sub { $loop->stop },
  262. );
  263. eval {
  264. $loop->run;
  265. } or my $e = $@;
  266. die $e if $e;
  267. exit 0;
  268. {
  269. my $json_id;
  270. my $requests;
  271. sub send_verto_json_request
  272. {
  273. $json_id ||= 1;
  274. my ($method, $params) = @_;
  275. my $json = {
  276. jsonrpc => "2.0",
  277. method => $method,
  278. params => $params,
  279. id => $json_id,
  280. };
  281. my $text = JSON->new->encode( $json );
  282. warn "[Verto] sending $text";
  283. $bot_verto->send_frame ( $text );
  284. my $request = $loop->new_future;
  285. $requests->{$json_id} = $request;
  286. $json_id++;
  287. return $request;
  288. }
  289. sub send_verto_json_response
  290. {
  291. my ($result, $id) = @_;
  292. my $json = {
  293. jsonrpc => "2.0",
  294. result => $result,
  295. id => $id,
  296. };
  297. my $text = JSON->new->encode( $json );
  298. warn "[Verto] sending $text";
  299. $bot_verto->send_frame ( $text );
  300. }
  301. sub on_verto_json
  302. {
  303. my $json = JSON->new->decode( $_[0] );
  304. if ($json->{method}) {
  305. if (($json->{method} eq 'verto.answer' && $json->{params}->{sdp}) ||
  306. $json->{method} eq 'verto.media') {
  307. my $caller = $json->{dialogParams}->{caller_id_number};
  308. my $callee = $json->{dialogParams}->{destination_number};
  309. my $caller_user = '@+' . $caller . ':' . $hs_domain;
  310. my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";
  311. my $room_id = $roomid_by_callid->{$json->{params}->{callID}};
  312. if ($json->{params}->{sdp}) {
  313. $http->do_request(
  314. method => "POST",
  315. uri => URI->new(
  316. $CONFIG{"matrix"}->{server}.
  317. "/_matrix/client/api/v1/send/m.call.answer?".
  318. "access_token=$as_token&user_id=$caller_user"
  319. ),
  320. content_type => "application/json",
  321. content => JSON->new->encode({
  322. call_id => $bridgestate->{$room_id}->{matrix_callid},
  323. version => 0,
  324. answer => {
  325. sdp => $json->{params}->{sdp},
  326. type => "answer",
  327. },
  328. }),
  329. )->then( sub {
  330. send_verto_json_response( {
  331. method => $json->{method},
  332. }, $json->{id});
  333. })->get;
  334. }
  335. }
  336. elsif ($json->{method} eq 'verto.invite') {
  337. my $caller = $json->{dialogParams}->{caller_id_number};
  338. my $callee = $json->{dialogParams}->{destination_number};
  339. my $caller_user = '@+' . $caller . ':' . $hs_domain;
  340. my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";
  341. my $alias = ($caller lt $callee) ? ($caller.'-'.$callee) : ($callee.'-'.$caller);
  342. my $room_id;
  343. # create a virtual user for the caller if needed.
  344. create_virtual_user($caller);
  345. # create a room of form #peer-peer and invite the callee
  346. $http->do_request(
  347. method => "POST",
  348. uri => URI->new(
  349. $CONFIG{"matrix"}->{server}.
  350. "/_matrix/client/api/v1/createRoom?".
  351. "access_token=$as_token&user_id=$caller_user"
  352. ),
  353. content_type => "application/json",
  354. content => JSON->new->encode({
  355. room_alias_name => $alias,
  356. invite => [ $callee_user ],
  357. }),
  358. )->then( sub {
  359. my ( $response ) = @_;
  360. my $resp = JSON->new->decode($response->content);
  361. $room_id = $resp->{room_id};
  362. $roomid_by_callid->{$json->{params}->{callID}} = $room_id;
  363. })->get;
  364. # join it
  365. my ($response) = $http->do_request(
  366. method => "POST",
  367. uri => URI->new(
  368. $CONFIG{"matrix"}->{server}.
  369. "/_matrix/client/api/v1/join/$room_id?".
  370. "access_token=$as_token&user_id=$caller_user"
  371. ),
  372. content_type => "application/json",
  373. content => '{}',
  374. )->get;
  375. $bridgestate->{$room_id}->{matrix_callid} = lc new Data::UUID->create_str();
  376. $bridgestate->{$room_id}->{callid} = $json->{dialogParams}->{callID};
  377. $bridgestate->{$room_id}->{sessid} = $sessid;
  378. # put the m.call.invite in there
  379. $http->do_request(
  380. method => "POST",
  381. uri => URI->new(
  382. $CONFIG{"matrix"}->{server}.
  383. "/_matrix/client/api/v1/send/m.call.invite?".
  384. "access_token=$as_token&user_id=$caller_user"
  385. ),
  386. content_type => "application/json",
  387. content => JSON->new->encode({
  388. call_id => $bridgestate->{$room_id}->{matrix_callid},
  389. version => 0,
  390. answer => {
  391. sdp => $json->{params}->{sdp},
  392. type => "offer",
  393. },
  394. }),
  395. )->then( sub {
  396. # acknowledge the verto
  397. send_verto_json_response( {
  398. method => $json->{method},
  399. }, $json->{id});
  400. })->get;
  401. }
  402. elsif ($json->{method} eq 'verto.bye') {
  403. my $caller = $json->{dialogParams}->{caller_id_number};
  404. my $callee = $json->{dialogParams}->{destination_number};
  405. my $caller_user = '@+' . $caller . ':' . $hs_domain;
  406. my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";
  407. my $room_id = $roomid_by_callid->{$json->{params}->{callID}};
  408. # put the m.call.hangup into the room
  409. $http->do_request(
  410. method => "POST",
  411. uri => URI->new(
  412. $CONFIG{"matrix"}->{server}.
  413. "/_matrix/client/api/v1/send/m.call.hangup?".
  414. "access_token=$as_token&user_id=$caller_user"
  415. ),
  416. content_type => "application/json",
  417. content => JSON->new->encode({
  418. call_id => $bridgestate->{$room_id}->{matrix_callid},
  419. version => 0,
  420. }),
  421. )->then( sub {
  422. # acknowledge the verto
  423. send_verto_json_response( {
  424. method => $json->{method},
  425. }, $json->{id});
  426. })->get;
  427. }
  428. else {
  429. warn ("[Verto] unhandled method: " . $json->{method});
  430. send_verto_json_response( {
  431. method => $json->{method},
  432. }, $json->{id});
  433. }
  434. }
  435. elsif ($json->{result}) {
  436. $requests->{$json->{id}}->done($json->{result});
  437. }
  438. elsif ($json->{error}) {
  439. $requests->{$json->{id}}->fail($json->{error}->{message}, $json->{error});
  440. }
  441. }
  442. }