File Coverage

blib/lib/Centrifugo/Client.pm
Criterion Covered Total %
statement 123 250 49.2
branch 18 80 22.5
condition 12 43 27.9
subroutine 24 45 53.3
pod 0 12 0.0
total 177 430 41.1


line stmt bran cond sub pod time code
1             package Centrifugo::Client;
2            
3             our $VERSION = "1.05";
4            
5 1     1   51969 use Exporter;
  1         2  
  1         59  
6             our @ISA = qw(Exporter);
7             our @EXPORT = qw(generate_token);
8            
9 1     1   5 use Carp qw( croak );
  1         1  
  1         47  
10 1     1   385 use AnyEvent::WebSocket::Client 0.40; # Version needed for reason when close. See https://github.com/plicease/AnyEvent-WebSocket-Client/issues/30
  1         195589  
  1         37  
11 1     1   453 use AnyEvent::HTTP;
  1         6727  
  1         71  
12 1     1   487 use JSON;
  1         7714  
  1         6  
13            
14             =head1 NAME
15            
16             Centrifugo::Client
17            
18             =head1 SYNOPSIS
19            
20             use Centrifugo::Client;
21             use AnyEvent;
22            
23             my $cclient = Centrifugo::Client->new("$CENTRIFUGO_WS/connection/websocket");
24            
25             $cclient -> on('connect', sub{
26             my ($infoRef)=@_;
27             print "Connected to Centrifugo version ".$infoRef->{version};
28             }) -> on('message', sub{
29             my ($infoRef)=@_;
30             print "MESSAGE: ".encode_json $infoRef->{data};
31            
32             }) -> connect(
33             user => $USER_ID,
34             timestamp => $TIMESTAMP,
35             token => $TOKEN
36             );
37            
38             $cclient->subscribe( channel => 'my-channel&' );
39             $cclient->subscribe( channel => 'public-channel' );
40             $cclient->subscribe( channel => '$private' );
41            
42             # Now start the event loop to keep the program alive
43             AnyEvent->condvar->recv;
44            
45             =head1 DESCRIPTION
46            
47             This library allows to communicate with Centrifugo through a websocket.
48            
49             =cut
50            
51 1     1   134 use strict;
  1         2  
  1         22  
52 1     1   8 use warnings;
  1         3  
  1         2152  
53            
54            
55             =head1 FUNCTION new
56            
57             my $client = Centrifugo::Client->new( $URL );
58            
59             or
60            
61             my $client = Centrifugo::Client->new( $URL,
62             debug => 'true', # If true, some informations are written on STDERR
63             debug_ws => 'true', # If true, all web socket messages are written on STDERR
64             authEndpoint => "...", # The full URL used to ask for a key to subscribe to private channels
65             max_alive_period => 30, # interval (in s) since last communication with server that triggers a PING (default 0)
66             refresh_period => 5, # Check frequency for max_alive_period (default 10s)
67             retry => 0.5 , # interval (in ms) between reconnect attempts which value grows exponentially (default 1.0)
68             max_retry => 30, # upper interval value limit when reconnecting. (default 30)
69             resubscribe => 'true', # automatic resubscribing on subscriptions (default: 'true')
70             recover => 'true', # Recovers the lost messages after a reconnection (default: 'false')
71             ws_params => { # These parameters are passed to AnyEvent::WebSocket::Client->new(...)
72             ssl_no_verify => 'true',
73             timeout => 600
74             },
75             );
76            
77             =cut
78            
79             sub new {
80 1     1 0 24 my ($class, $ws_url, %params)=@_;
81 1         6 my $this = {};
82 1         4 bless($this, $class);
83 1         14 $this->{WS_URL} = $ws_url;
84 1   33     9 $this->{DEBUG} = $params{debug} && $params{debug}!~/^(0|false|no)$/i; delete $params{debug};
  1         4  
85 1   33     10 $this->{DEBUG_WS} = $params{debug_ws} && $params{debug_ws}!~/^(0|false|no)$/i; delete $params{debug_ws};
  1         3  
86 1   50     11 $this->{AUTH_URL} = delete $params{authEndpoint} || "/centrifuge/auth/";
87 1         4 $this->{WEBSOCKET} = AnyEvent::WebSocket::Client -> new( %{$params{ws_params}} ); delete $params{ws_params};
  1         18  
  1         3343  
88 1   50     12 $this->{MAX_ALIVE} = delete $params{max_alive_period} || 0;
89 1   50     10 $this->{REFRESH} = delete $params{refresh_period} || 10;
90 1   50     10 $this->{RETRY} = delete $params{retry} || 1;
91 1   50     10 $this->{MAX_RETRY} = delete $params{max_retry} || 30;
92 1   33     10 $this->{RESUBSCRIBE} = ! defined $params{resubscribe} || $params{resubscribe}!~/^(0|false|no)$/i; delete $params{resubscribe};
  1         4  
93 1   33     7 $this->{RECOVER} = $params{recover} && $params{recover}!~/^(0|false|no)$/i; delete $params{recover};
  1         4  
94 1 50       7 croak "Centrifugo::Client : Unknown parameter : ".join',',keys %params if %params;
95 1         6 return $this;
96             }
97            
98             =head1 FUNCTION connect - send authorization parameters to Centrifugo so your connection could start subscribing on channels.
99            
100             $client->connect(
101             user => $USER_ID,
102             timestamp => $TIMESTAMP,
103             token => $TOKEN,
104             [info => $info,]
105             [uid => $uid,]
106             );
107            
108             This function retuns $self to allow chains of multiple function calls.
109            
110             It is possible to provide a UID for this command, but if you don't, a random one will be generated for you and cannot be retrieved afterward.
111            
112             =cut
113            
114             sub connect {
115 1     1 0 8 my ($this,%PARAMS) = @_;
116 1 50       7 croak("Missing user in Centrifugo::Client->connect(...)") if ! $PARAMS{user};
117 1 50       6 croak("Missing timestamp in Centrifugo::Client->connect(...)") if ! $PARAMS{timestamp};
118 1 50       5 croak("Missing token in Centrifugo::Client->connect(...)") if ! $PARAMS{token};
119             # Fix parameters sent to Centrifugo
120 1 50       8 $PARAMS{timestamp}="$PARAMS{timestamp}" if $PARAMS{timestamp}; # This MUST be a string
121             # Save the Centrifugo connection parameters
122 1   33     8 $this->{_cnx_uid} = delete $PARAMS{uid} || _generate_random_id();
123 1         5 $this->{_cnx_params} = \%PARAMS;
124            
125             # Connects to Websocket
126 1         8 $this->_reset_reconnect_sequence();
127 1         7 $this->_connect();
128 1         17546 return $this;
129             }
130            
131             # This function (re)connects to the websocket
132             sub _connect {
133 1     1   4 my ($this) = @_;
134             $this->{WEBSOCKET}->connect( $this->{WS_URL} )->cb(sub {
135 1     1   208889 $this->{WSHANDLE} = eval { shift->recv };
  1         7  
136 1 50       12 if ($@) {
137 0         0 $this->_on_error($@);
138 0         0 $this->_reconnect();
139 0         0 return;
140             }
141             # The websocket connection is OK
142 1         4 $this->_on_ws_connect();
143 1         21 });
144             }
145            
146             # This function is called when client is connected to the WebSocket
147             sub _on_ws_connect {
148 1     1   2 my ($this) = @_;
149 1         7 $this->_debug( "Centrifugo::Client : WebSocket connected to $this->{WS_URL}" );
150            
151             # define the callbacks
152 1     1   9 $this->{WSHANDLE}->on(each_message => sub { $this->_on_ws_message($_[1]) });
  1         57090  
153 1     0   29 $this->{WSHANDLE}->on(finish => sub { $this->_on_close(($_[0])->close_reason()) });
  0         0  
154             $this->{WSHANDLE}->on(parse_error => sub {
155 0     0   0 my($cnx, $error) = @_;
156 0         0 $this->_debug( "Error in Centrifugo::Client : $error" );
157 0 0       0 $this->{ON}->{'error'}->($error) if $this->{ON}->{'error'};
158 1         13 });
159            
160             # Then, connects to Centrifugo
161             $this->_send_message( {
162             method => 'connect',
163             UID => $this->{_cnx_uid},
164             params => $this->{_cnx_params}
165 1         13 } );
166             }
167            
168             # This function is called when client is connected to Centrifugo
169             sub _on_connect {
170 1     1   4 my ($this, $body) = @_;
171 1         18 $this->_debug( "Centrifugo::Client : Connected to Centrifugo : ".encode_json $body );
172             # on Connect, the client_id must be read (if available)
173 1 50 33     17 if ($body && ref($body) eq 'HASH' && $body->{client}) {
      33        
174 1         4 $this->{CLIENT_ID} = $body->{client};
175 1         6 $this->_debug( "Centrifugo::Client : CLIENT_ID=".$this->{CLIENT_ID} );
176             }
177 1 50       6 $this->_init_keep_alive_timer() if $this->{MAX_ALIVE};
178 1         6 $this->_reset_reconnect_sequence();
179 1 50       7 $this->_resubscribe() if $this->{RESUBSCRIBE};
180             }
181            
182             # This function is called when client receives a message
183             sub _on_message {
184 0     0   0 my ($this, $body) = @_;
185 0         0 my $uid = $body->{uid};
186 0         0 my $channel = $body->{channel};
187 0         0 $this->_debug( "Centrifugo::Client : Message from $channel : ".encode_json $body->{data} );
188 0         0 $this->{_channels}->{ $channel }->{last} = $uid; # Keeps track of last IDs of messages
189             }
190            
191             # This function is called when client is connected to Centrifugo
192             sub _on_subscribe {
193 0     0   0 my ($this, $body) = @_;
194 0         0 my $channel = $body->{channel};
195 0         0 $this->_debug( "Centrifugo::Client : Subscribed to $channel : ".encode_json $body );
196 0 0       0 if ($body->{recovered} == JSON::true) {
197             # Re-emits the lost messages
198 0         0 my $messages = $body->{messages};
199 0         0 foreach my $message (reverse @$messages) {
200 0         0 $this->_on_message($message);
201 0         0 my $sub = $this->{ON}->{message};
202 0 0       0 $sub->($message) if $sub;
203             }
204             }
205             # Keeps track of channels
206 0         0 $channel=~s/&.*/&/; # Client channel boundary
207 0         0 $this->{_channels}->{ $channel } = $body;
208 0         0 $this->{_subscribed_channels}->{ $channel } = 1; # TEST if it worked
209 0         0 delete $this->{_pending_subscriptions}->{ $channel };
210             }
211            
212             # This function is called when client is connected to Centrifugo
213             sub _on_unsubscribe {
214 0     0   0 my ($this, $body) = @_;
215 0         0 my $channel = $body->{channel};
216 0         0 $this->_debug( "Centrifugo::Client : Unsubscribed from $body->{channel} : ".encode_json $body );
217             # Keeps track of channels
218 0         0 $channel=~s/&.*/&/; # Client channel boundary
219 0         0 delete $this->{_channels}->{ $channel };
220 0         0 delete $this->{_subscribed_channels}->{ $channel };
221 0         0 delete $this->{_pending_subscriptions}->{ $channel };
222             }
223            
224             # This function automatically reconnects to channels
225             sub _resubscribe {
226 1     1   4 my ($this) = @_;
227 1         4 foreach my $channel (keys %{$this->{_channels}}) {
  1         7  
228 0         0 $this->_debug( "Centrifugo::Client : Resubscribe to $channel" );
229 0         0 $channel=~s/&.*/&/; # Client channel boundary
230 0         0 my $params = {
231             channel => $channel
232             };
233 0 0 0     0 if ($this->{RECOVER} && $this->{_channels}->{$channel}->{last}) {
234 0         0 $params->{recover}=JSON::true;
235 0         0 $params->{last}=$this->{_channels}->{$channel}->{last};
236             }
237 0         0 $this->subscribe( %$params );
238             }
239             }
240            
241             # This function is called when the connection with server is lost
242             sub _on_close {
243 0     0   0 my ($this, $message) = @_;
244 0 0       0 $message="(none)" unless $message;
245 0         0 $this->_debug( "Centrifugo::Client : Connection closed, reason=$message" );
246 0 0       0 $this->{ON}->{'ws_closed'}->($message) if $this->{ON}->{'ws_closed'};
247 0         0 undef $this->{_alive_handler};
248 0         0 undef $this->{WSHANDLE};
249 0         0 undef $this->{CLIENT_ID};
250 0         0 delete $this->{_subscribed_channels};
251 0         0 delete $this->{_pending_subscriptions};
252 0         0 $this->_reconnect();
253             }
254            
255             # This function is called if an errors occurs with the server
256             sub _on_error {
257 0     0   0 my ($this, @infos) = @_;
258 0         0 warn "Error in Centrifugo::Client : @infos";
259 0 0       0 $this->{ON}->{'error'}->(@infos) if $this->{ON}->{'error'};
260             }
261            
262             # This function is called once for each message received from Centrifugo
263             sub _on_ws_message {
264 1     1   4 my ($this, $message) = @_;
265 1         10 $this->_debug_ws("Send > WebSocket : $message->{body}");
266 1         5 $this->{_last_alive_message} = time();
267 1         19 my $fullbody = decode_json($message->{body}); # The body of websocket message
268             # Handle a body containing {response} : converts into a singleton
269 1 50       7 if (ref($fullbody) eq 'HASH') {
270 1         5 $fullbody = [ $fullbody ];
271             }
272             # Handle the body which is now an array of response
273 1         4 foreach my $info (@$fullbody) {
274 1         4 my $uid = $info->{uid};
275 1         4 my $method = $info->{method};
276 1         4 my $body = $info->{body}; # The body of Centrifugo message
277 1 50       10 $this->_on_connect( $body ) if $method eq 'connect';
278 1 50       9 $this->_on_subscribe( $body ) if $method eq 'subscribe';
279 1 50       5 $this->_on_unsubscribe( $body ) if $method eq 'unsubscribe';
280 1 50       6 $this->_on_message( $body ) if $method eq 'message';
281            
282             # Call the callback of the method
283 1         5 my $sub = $this->{ON}->{$method};
284 1 50       10 if ($sub) { # TODO : CHECK THIS !!!
285             # Add UID into body if available
286 1 50       4 if ($uid) {
287 1         5 $body->{uid}=$uid;
288             }
289 1         8 $sub->( $body );
290             }
291             }
292             }
293            
294             # Inits the Fibonacci sequence for reconnection retries
295             sub _reset_reconnect_sequence {
296 2     2   9 my ($this) = @_;
297 2         6 $this->{_last_retry} = 0;
298 2         10 $this->{_next_retry} = $this->{RETRY};
299             }
300            
301             # Reconnects to the server after a loss of connection
302             # When client disconnected from server it will automatically try to reconnect using
303             # fibonacci sequence to get interval between reconnect attempts which value grows exponentially. (why not ?)
304             sub _reconnect {
305 0     0   0 my ($this) = @_;
306 0 0       0 my $retry_after = $this->{_next_retry} > $this->{MAX_RETRY} ? $this->{MAX_RETRY} : $this->{_next_retry};
307 0 0       0 $retry_after = int($retry_after) if $retry_after > 3;
308 0         0 $this->_debug( "Centrifugo::Client : will reconnect after $retry_after s." );
309             $this->{reconnect_handler} = AnyEvent->timer(
310             after => $retry_after,
311             cb => sub {
312 0     0   0 $this->{_next_retry} += $this->{_last_retry};
313 0         0 $this->{_last_retry} = $retry_after;
314 0         0 $this->_connect();
315             }
316 0         0 );
317             }
318            
319             # Creates the timer to send periodic ping
320             sub _init_keep_alive_timer {
321 0     0   0 my ($this) = @_;
322             $this->{_alive_handler} = AnyEvent->timer(
323             after => $this->{REFRESH},
324             interval => $this->{REFRESH},
325             cb => sub {
326 0     0   0 my $late = time() - $this->{_last_alive_message};
327 0 0       0 if ($late > $this->{MAX_ALIVE}) {
328 0         0 $this->_debug( "Sending ping (${late}s without message)" );
329 0         0 $this->ping();
330             }
331             }
332 0         0 );
333             }
334            
335             =head1 FUNCTION publish - allows clients directly publish messages into channel (use with caution. Client->Server communication is NOT the aim of Centrifugo)
336            
337             $client->publish( channel=>$channel, data=>$data, [uid => $uid] );
338            
339             $data must be a HASHREF to a structure (which will be encoded to JSON), for example :
340            
341             $client->public ( channel => "public",
342             data => {
343             nick => "Anonymous",
344             text => "My message",
345             } );
346            
347             or even :
348            
349             $client->public ( channel => "public", data => { } ); # Sends an empty message to the "public" channel
350            
351             This function returns the UID used to send the command to the server. (a random string if none is provided)
352             =cut
353            
354             sub publish {
355 0     0 0 0 my ($this, %PARAMS) = @_;
356 0 0       0 croak("Missing channel in Centrifugo::Client->publish(...)") unless $PARAMS{channel};
357 0 0       0 croak("Missing data in Centrifugo::Client->publish(...)") unless $PARAMS{data};
358 0   0     0 my $uid = $PARAMS{'uid'} || _generate_random_id();
359 0         0 delete $PARAMS{'uid'};
360 0         0 $PARAMS{channel}=~s/&.*/'&' . $this->client_id()/e; # Client channel boundary
  0         0  
361 0         0 $this->_send_message({
362             UID => $uid,
363             method => 'publish',
364             params => \%PARAMS
365             });
366 0         0 return $uid;
367             }
368            
369             =head1 FUNCTION disconnect
370            
371             $client->disconnect();
372            
373             =cut
374            
375             sub disconnect {
376 0     0 0 0 my ($this) = @_;
377 0 0       0 $this->{WSHANDLE}->close() if $this->{WSHANDLE};
378 0         0 my $sub = $this->{ON}->{'disconnect'};
379 0 0       0 $sub->() if $sub;
380             }
381            
382             =head1 FUNCTION subscribe - allows to subscribe on channel after client successfully connected.
383            
384             $client->subscribe( channel => $channel, [ uid => $uid ,] );
385            
386             If the channel is private (starts with a '$'), then a request to $this->{AUTH_URL} is done automatically to get the channel key.
387            
388             If channel contains a '&', then the function adds the client_id behind the client channel boundary. (even after a reconnect)
389            
390             This function returns the UIDs used to send the command to the server. (a random string if none is provided)
391            
392             =cut
393            
394             sub subscribe {
395 0     0 0 0 my ($this, %PARAMS) = @_;
396 0         0 my $channel = $PARAMS{channel};
397 0         0 $channel=~s/&.*/&/; # Client channel boundary
398            
399 0 0       0 next if $this->{_subscribed_channels}->{ $channel };
400 0 0       0 next if $this->{_pending_subscriptions}->{ $channel };
401            
402             # If the client is not connected, then delay the subscribing
403 0 0       0 unless ($this->client_id()) {
404 0         0 my $error = "Can't subscribe to channel '$channel' yet : Client is not connected (will try again when connected)";
405 0         0 $this->_debug( "Error in Centrifugo::Client : $error" );
406 0 0       0 $this->{ON}->{'error'}->($error) if $this->{ON}->{'error'};
407             # Register the channel so that we can subscribe when the client is connected
408 0         0 $this->{_channels}->{ $channel } = { status => JSON::false };
409 0         0 return undef;
410             }
411 0         0 $this->{_pending_subscriptions}->{ $channel } = 1; # Don't subscribe again
412            
413 0         0 my $SUBREQ = {
414             channel => $channel
415             };
416 0 0       0 $SUBREQ->{uid} = $PARAMS{uid} if $PARAMS{uid};
417            
418             # Direct subscribe of non-private channels
419 0 0       0 return _channel_command($this,'subscribe',%$SUBREQ) unless $channel=~/^\$/; # Private channels starts with $
420            
421             # If the channel is private, then an API-call to /centrifuge/auth/ must be done
422 0         0 $SUBREQ->{ client } = $this->client_id();
423            
424             # Request a channel key
425 0         0 my $data = encode_json {
426             client => $this->client_id(),
427             channels => [ $channel ]
428             };
429 0         0 my $URL = $this->{AUTH_URL};
430             http_post $URL, $data,
431             headers => {
432             contentType => "application/json"
433             },
434             sub {
435 0     0   0 my ($data, $headers) = @_;
436 0 0       0 unless ($headers->{Status}==200) {
437             # Can't access to URL (TODO : should we retry for this ?)
438 0         0 my $error = "Can't subscribe to channel '$channel' : Couldn't connect to $URL : Status=".$headers->{Status};
439 0 0       0 $this->{ON}->{'error'}->($error) if $this->{ON}->{'error'};
440 0         0 return;
441             }
442 0         0 my $result = decode_json $data;
443 0         0 my $key = $result->{$channel}->{sign};
444 0         0 $SUBREQ->{sign} = $key;
445             # The request is now complete : {channel: "...", client:"...", sign:"..."}
446 0         0 return _channel_command($this,'subscribe',%$SUBREQ);
447 0         0 };
448             }
449            
450             sub _channel_command {
451 0     0   0 my ($this,$command,%PARAMS) = @_;
452 0         0 $PARAMS{channel} =~s /&.*/'&' . $this->client_id()/e; # Client channel boundary
  0         0  
453 0   0     0 my $uid = $PARAMS{'uid'} || _generate_random_id();
454 0         0 my $MSG = {
455             UID => $uid ,
456             method => $command,
457             params => \%PARAMS
458             };
459 0         0 $this->_send_message($MSG);
460 0         0 return $uid;
461             }
462            
463             =head1 FUNCTION unsubscribe - allows to unsubscribe from channel.
464            
465             $client->unsubscribe( channel => $channel, [ uid => $uid ] );
466            
467             This function returns the UID used to send the command to the server. (a random string if none is provided)
468            
469             =cut
470            
471             sub unsubscribe {
472 0     0 0 0 my ($this, %PARAMS) = @_;
473 0         0 return _channel_command($this,'unsubscribe',%PARAMS);
474             }
475            
476             =head1 FUNCTION presence - allows to ask server for channel presence information.
477            
478             $client->presence( channel => $channel, [ uid => $uid ] );
479            
480             This function returns the UID used to send the command to the server. (a random string if none is provided)
481            
482             =cut
483            
484             sub presence {
485 0     0 0 0 my ($this, %PARAMS) = @_;
486 0         0 return _channel_command($this,'presence',%PARAMS);
487             }
488            
489             =head1 FUNCTION history - allows to ask server for channel presence information.
490            
491             $client->history( channel => $channel, [ uid => $uid ] );
492            
493             This function returns the UID used to send the command to the server. (a random string if none is provided)
494            
495             =cut
496            
497             sub history {
498 0     0 0 0 my ($this, %PARAMS) = @_;
499 0         0 return _channel_command($this,'history',%PARAMS);
500             }
501            
502             =head1 FUNCTION ping - allows to send ping command to server, server will answer this command with ping response.
503            
504             $client->ping( [ uid => $uid ] );
505            
506             This function returns the UID used to send the command to the server. (a random string if none is provided)
507            
508             =cut
509            
510             sub ping {
511 0     0 0 0 my ($this,%PARAMS) = @_;
512 0   0     0 my $uid = $PARAMS{'uid'} || _generate_random_id();
513 0         0 my $MSG = {
514             UID => $uid ,
515             method => 'ping'
516             };
517 0         0 $this->_send_message($MSG);
518 0         0 return $uid;
519             }
520            
521             =head1 FUNCTION on - Register a callback for the given event.
522            
523             Known events are 'message', 'connect', 'disconnect', 'subscribe', 'unsubscribe', 'publish', 'presence', 'history', 'join', 'leave',
524             'refresh', 'ping', 'ws_closed', 'ws_error'
525            
526             $client->on( 'connect', sub {
527             my( $dataRef ) = @_;
528             ...
529             });
530            
531             (this function retuns $self to allow chains of multiple function calls)
532            
533             Note : Events that are an answer to the client requests (ie 'connect', 'publish', ...) have an 'uid' which is added into the %data structure.
534            
535             =cut
536            
537             sub on {
538 4     4 0 29 my ($this, $method, $sub)=@_;
539 4         18 $this->{ON}->{$method} = $sub;
540 4         41 $this;
541             }
542            
543             =head1 FUNCTION client_id - return the client_id if it is connected to Centrifugo and the server returned this ID (which is not the case on the demo server).
544            
545             $client->client_id()
546            
547             =cut
548            
549             sub client_id {
550 0     0 0 0 my ($this)=@_;
551 0         0 return $this->{CLIENT_ID};
552             }
553            
554            
555             =head1 FUNCTION generate_token - return the private token that must be used to connect a client to Centrifugo.
556            
557             $key = Centrifugo::Client::generate_token($secret, $user, $timestamp [,$info])
558            
559             INPUT : $secret is the private secret key, only known by the server.
560            
561             $user is the user name.
562            
563             $timestamp is the current timestamp.
564            
565             $info is a JSON encoded string.
566            
567             The same function may be used to generate a private channel key :
568            
569             $key = generate_token($secret, $client, $channel [,$info])
570            
571             INPUT : $client is the client_id given when connected to Centrifugo.
572            
573             $channel is the name of the channel (should start with a '$' as it is private).
574            
575             And to sign each request to access to the HTTP API :
576            
577             $sign = generate_token($self, $data)
578            
579             INPUT : $data is a JSON string with your API commands
580            
581             =cut
582            
583             sub generate_token {
584 1     1 0 4899 my ($secret, @infos)=@_;
585 1         6 my $info = join'', @infos;
586 1     1   7 use Digest::SHA qw( hmac_sha256_hex );
  1         2  
  1         299  
587 1         28 return hmac_sha256_hex( $info, $secret );
588             }
589            
590             ##### (kinda)-private functions
591            
592             sub _send_message {
593 1     1   3 my ($this,$MSG)=@_;
594 1         12 $MSG = encode_json $MSG;
595 1         5 $this->_debug_ws("Send > WebSocket : $MSG");
596 1         4 $this->{WSHANDLE}->send($MSG);
597             }
598            
599             sub _debug {
600 3     3   9 my ($this,$MSG)=@_;
601 3         12 local $\; $\="\n";
  3         10  
602 3 50       16 print STDERR "Centrifugo::Client : $MSG" if $this->{DEBUG};
603             }
604            
605             sub _debug_ws {
606 2     2   7 my ($this,$MSG)=@_;
607 2         9 local $\; $\="\n";
  2         6  
608 2 50       11 print STDERR "Centrifugo::Client : $MSG" if $this->{DEBUG_WS};
609             }
610            
611            
612             # Generates a random Id for commands
613             sub _generate_random_id {
614 1     1   21 my @c = ('a'..'z','A'..'Z',0..9);
615 1         6 return join '', @c[ map{ rand @c } 1 .. 12 ];
  12         56  
616             }
617             1;