apps / sms / bot /
Newer Older
459 lines | 15.345kb
initial commit
admin cloud-section (root) authored on 2016-12-10
1
#!/usr/bin/perl
2

            
3
use strict;
4
use warnings;
5

            
6
use Net::XMPP;
7
use DBI;
8
use POSIX qw/ceil strftime/;
9

            
10
use open ':utf8';
11
use open ':std';
12
use utf8;
13
#use Data::Dumper;
14

            
15
use threads;
16
use threads::shared;
17
use Thread::Queue;
18

            
19
my $scriptconf = $ENV{PWD} . "/$0";
20
$scriptconf =~ s/\.pl$//;
21
$scriptconf =~ s/$/.conf/;
22
if (-r $scriptconf) {
23
    package cfg;
24
    unless (my $return = do $scriptconf) {
25
        warn "couldn't parse $scriptconf: $@" if $@;
26
        warn "couldn't do $scriptconf: $!"    unless defined $return;
27
        warn "couldn't run $scriptconf"       unless $return;
28
    }
29
}
30
else {
31
    print "pas de config\n";
32
    exit;
33
}
34

            
35
$SIG{INT}=\&terminate;
36
#my $next_sms :shared; # utilisé pour le top du procahin envoi
37
my $debugLevel = 0;
38

            
39
my $maxs_status : shared = "";
40

            
41
my $from_gtalksms_queue = Thread::Queue->new;
42
my $to_gtalksms_queue = Thread::Queue->new;
43
my $mail_queue = Thread::Queue->new;
44

            
45
my $bot = &xmpp_login($cfg::config{xmpp}->{hostName}, 
46
                      $cfg::config{xmpp}->{portNumber}, 
47
                      $cfg::config{xmpp}->{userName}, 
48
                      $cfg::config{xmpp}->{passWord}, 
49
                      $cfg::config{xmpp}->{componentName}, 
50
                      $cfg::config{xmpp}->{resource}, 
51
                      $cfg::config{xmpp}->{tls}, 
52
                      1, "", 0, 
53
                      $cfg::config{DEBUG});
54

            
55
$SIG{INT} = sub { $bot->Disconnect; };
56

            
57
my $mail = threads->new(\&ssmtp_send_mail);
58

            
59
my $from_gtalksms = threads->new(\&from_gtalksms_parse);
60

            
61
my $to_gtalksms = threads->new(\&xmpp_send_sms);
62

            
63
sub debug_print {
64
    print STDERR "sendxmpp: " . (join ' ', @_) . "\n"
65
	if (@_ && ($cfg::config{DEBUG} ||$cfg::config{VERBOSE}));
66
}
67

            
68
sub log_bot {
69
#    open(LOGFILE,'>>', 'bot.log');
70
#    print LOGFILE sprintf("[%s] %s\n",strftime("%Y-%m-%d %H:%M:%S", localtime), shift);
71
    print  sprintf("[%s] %s\n",strftime("%Y-%m-%d %H:%M:%S", localtime), shift);
72
#    close LOGFILE;
73
}
74

            
75
sub xmpp_logout($) {
76
    # HACK
77
    # messages may not be received if we log out too quickly...
78
    sleep 1;
79

            
80
    my $cnx = shift;
81
    $cnx->Disconnect();
82
    xmpp_check_result ('Disconnect',0); # well, nothing to check, really
83
}
84

            
85
sub terminate () {
86
    my $cnx = shift;
87
    debug_print "caught TERM";
88
    xmpp_logout($cnx);
89
    exit 0;
90
}
91

            
92
sub error_exit {
93
    my ($err,$cnx) = @_;
94
    print STDERR "$err\n";
95
    xmpp_logout ($cnx)
96
	if ($cnx);
97
    exit 1;
98
}
99

            
100
sub xmpp_check_result {
101
    my ($txt, $res, $cnx)=@_;
102

            
103
    error_exit ("Error '$txt': result undefined")
104
	unless (defined $res);
105

            
106
    # res may be 0
107
	if ($res == 0) {
108
		debug_print "$txt";
109
		# result can be true or 'ok'
110
	}
111
	elsif ((@$res == 1 && $$res[0]) || $$res[0] eq 'ok') {
112
		debug_print "$txt: " .  $$res[0];
113
		# otherwise, there is some error
114
	}
115
	else {
116
		my $errmsg = $cnx->GetErrorCode() || '?';
117
		error_exit ("Error '$txt': " . join (': ',@$res) . "[$errmsg]", $cnx);
118
	}
119
}
120

            
121
sub xmpp_login ($$$$$$$$$$$) {
122
    my ($host, $port, $user, $pw, $comp, $res, $tls, $no_tls_verify, $tls_ca_path, $ssl, $debug) = @_;
123
    my $cnx = new Net::XMPP::Client(debuglevel=>0);
124
    error_exit "could not create XMPP client object: $!" unless ($cnx);
125

            
126
	my $ssl_verify = 0x01;
127
	if ($no_tls_verify) { $ssl_verify = 0x00; }
128
	debug_print "ssl_verify: $ssl_verify";
129

            
130
	debug_print "tls_ca_path: $tls_ca_path";
131

            
132
    my @res;
133
	my $arghash = {
134
		hostname		=> $host,
135
		port            => $port,
136
		tls				=> $tls,
137
		ssl_verify		=> $ssl_verify,
138
		ssl_ca_path		=> $tls_ca_path,
139
		ssl             => $ssl,
140
		connectiontype	=> 'tcpip',
141
		componentname	=> $comp
142
	};
143

            
144
	delete $arghash->{port} unless $port; 
145
	if ($arghash->{port}) {
146
		@res = $cnx->Connect(%$arghash);
147
		error_exit ("Could not connect to '$host' on port $port: $@") unless @res;
148
	} else {
149
		@res = $cnx->Connect(%$arghash);
150
		error_exit ("Could not connect to server '$host': $@") unless @res;
151
	}
152

            
153
    xmpp_check_result("Connect",\@res,$cnx);
154

            
155
	if ($comp) {
156
		my $sid = $cnx->{SESSION}->{id};
157
		$cnx->{STREAM}->{SIDS}->{$sid}->{hostname} = $comp
158
	}
159

            
160
    @res = $cnx->AuthSend(#'hostname' => $host,
161
			  'username' => $user,
162
			  'password' => $pw,
163
			  'resource' => $res);
164
    xmpp_check_result('AuthSend',\@res,$cnx);
165

            
166
    return $cnx;
167
}
168

            
169
sub sql_request ($) {
170
    my $request = shift;
171
    my @result;
172
    my $dbh = DBI->connect($cfg::config{db}->{driver}, $cfg::config{db}->{user}, $cfg::config{db}->{password}, {'RaiseError' => 1});
173
    $dbh->{'mysql_enable_utf8'} = 1;
174
    $dbh->do(qq{SET NAMES "utf8"});
175
    my $sth = $dbh->prepare($request);
176
    $sth->execute();
177
    if (defined($sth->{NUM_OF_FIELDS})) {
178
        while (my $ref = $sth->fetchrow_hashref()) {
179
            push @result, $ref;
180
        }
181
    }
182
    $sth->finish();
183
    $dbh->disconnect();
184
    return @result;
185
}
186

            
187
sub massive_send_sms {
188
    my $request = shift;
189
    my @results = sql_request("SELECT * FROM __tables__ WHERE phone = '$request->{phone}' AND table_id = '$request->{table}'");
190
    if (scalar(@results) == 1) {
191
        my @results = sql_request("SELECT * FROM $request->{table}");
192
        my $start_msg = "envoi de " . scalar(@results) . " SMS (30 max par demi-heure)";
193
        $to_gtalksms_queue->enqueue([$request->{phone}, $start_msg]);
194
        foreach my $contact (@results) {
195
            $contact->{phone} =~ s/[\s\.]//g;
196
            $_ = $request->{body};
197
            s/\@prénom/$contact->{firstname}/g;
198
            if ($contact->{gender} eq 'F') {
199
                s/\@\(\s*(\w+)\s*,\s*\w+\s*\)/$1/g;
200
            }
201
            else {
202
                s/\@\(\s*\w+\s*,\s*(\w+)\s*\)/$1/g;
203
            }
204
            $to_gtalksms_queue->enqueue([$contact->{phone}, $_])
205
        }
206
        $to_gtalksms_queue->enqueue([$request->{phone}, "envoi des SMS terminé !"]);
207
    }
208
    else {
209
        $to_gtalksms_queue->enqueue([$request->{phone}, "pas la bonne base $request->{table} pour $request->{phone}"]);
210
    }
211
}
212

            
213
sub control_sms_flow {
214
    my ($body, $max, $interval) = @_;
215
    my $flow_control="/home/sms/flow.control";
216
#    use bytes; # les caractères UTF-8 peuvent être codés sur plus d'un octet
217
#    my $nbr = ceil(length($$body)/140); # taille d'un SMS 140 octets
218
     my $nbr = 1;
219
#    no bytes;
220
    while (1) {
221
        my $left = $max;
222
        my $t = time;
223
        open (FILE, "<", $flow_control);
224
        my @lines = <FILE>;
225
        close FILE;
226
        open(FILE, ">", $flow_control);
227
        foreach my $line (@lines) {
228
            $line =~ /(\d+)/;
229
            my $ts = $1;
230
            if ($ts + $interval > $t) {
231
                $left--;
232
                print FILE $line;
233
            }
234
        }
235
        close FILE;
236
        log_bot("  ---> to send: $nbr; left: $left");
237
        last if $nbr < $left;
238
        sleep 10;
239
    }
240
    open (FILE,">>", $flow_control);
241
    for (my $i = 0; $i < $nbr; $i++) { print FILE time, "\n"; }
242
    close FILE;
243
}
244

            
245
sub xmpp_send {
246
    my $msg = shift;
247
    $bot->MessageSend(
248
            to => $cfg::config{xmpp}->{maxs}, 
249
            from => $cfg::config{xmpp}->{userName} . "@" . $cfg::config{xmpp}->{hostName}, 
250
            resource => $cfg::config{xmpp}->{resource}, 
251
            type => 'chat', 
252
            body => $$msg
253
    );
254
}
255

            
256
sub ssmtp_send_mail {
257
    while (my $request = $mail_queue->dequeue) {
258
        defined($request->{to}) or $request->{to} = $cfg::config{mail};
259
        open(MAIL, "|/usr/lib/sendmail -t");
260
        print MAIL "Subject: $request->{subject}\n"; 
261
        print MAIL "To: $request->{to}\n";
262
        print MAIL "$request->{body}\n";
263
        close(MAIL);
264
        log_bot("mail envoyé à $request->{to}");
265
    }
266
}
267

            
268
sub wait_open_time ($$) {
269
    my ($close_hour, $open_hour) = @_;
270
    while (strftime('%H', localtime) > $close_hour or strftime('%H', localtime) < $open_hour) {
271
        sleep 1800;
272
    }
273
}
274

            
275
sub xmpp_send_sms {
276
    while (my $ref = $to_gtalksms_queue->dequeue) {
277
        wait_open_time(21, 8); # pas d'envoi entre 21h et 8h
278
#        TODO lock($next_sms);
279
#        TODO $next_sms = $$ref[0];
280
        control_sms_flow(\$$ref[1], 30, 1800); # pas plus de 30 messages de 140 caractères par demi-heure
281
        xmpp_send(\"sms:$$ref[0]:$$ref[1]");
282
        log_bot("envoi à $$ref[0] : $$ref[1]");
283
        sleep 10; # en attendant la maîtrise de cond_wait et cond_signal: cond_signal() called on unlocked variable at ./bot line 348
284
#        TODO cond_wait($next_sms); # attend le top 
285
    }
286
}
287

            
288
sub is_authorized {
289
    my ($requestor) = @_;
290
    my @results = sql_request("SELECT * FROM __authorized__ WHERE phone = '$requestor'");
291
    return (scalar(@results) == 1);
292
}
293

            
294
sub authorized_on_table {
295
    my %request = @_;
296
    $request{writeable} = (defined($request{writeable})) ? "AND write_auth = '1'" : "";
297
    my @results = sql_request("SELECT * FROM __tables__ WHERE table_id = '$request{table}' AND phone = '$request{id}' $request{writeable}");
298
    if (scalar(@results) == 0) {
299
        my @list = sql_request("SELECT * FROM __tables__ WHERE phone = '$request{id}'");
300
        my $list_str = "";
301
        foreach (@list) {
302
            $list_str .= "\n- $_->{table_id}";
303
        }
304
        ${$request{error}} = sprintf("%s%s%s%s", 
305
                "désolé, $request{table} n'est pas autorisé pour toi", 
306
                ($request{writeable} ne "")? " en écriture" : "", 
307
                "... ou n'existe pas:",
308
                $list_str);
309
    }
310
    return (scalar(@results) == 1);
311
}
312

            
313
sub is_table {
314
    my %request = @_;
315
    my @results = sql_request("SHOW TABLES WHERE Tables_in_sms = '$request{table}'");
316
    if (scalar(@results) == 0) {
317
        ${$request{error}} = "$request{table} n'existe pas... essaie la commande 'liste' pour voir tes tables";
318
    }
319
    return (scalar(@results) == 1);
320
}
321

            
322
sub copy {
323
    my $request = shift;
324
    my $rmsg = "";
325
    if (authorized_on_table(table => $request->{origin}, id => $request->{phone}, error => \$rmsg)) {
326
        if (! is_table(table => $request->{destination}, error => \$rmsg)) {
327
            sql_request("CREATE TABLE $request->{destination} SELECT * FROM $request->{origin}");
328
            sql_request("INSERT INTO __tables__ (phone,table_id,creation_date,update_date,comment,write_auth) values('"
329
                    . $request->{phone} . "','"
330
                    . $request->{destination} . "','"
331
                    . strftime("%Y-%m-%d %H:%M:%S", localtime) . "','"
332
                    . strftime("%Y-%m-%d %H:%M:%S", localtime) . "','"
333
                    . $request->{comment} . "','1')"
334
                    );
335
            $rmsg = "copie de $request->{origin} vers $request->{destination} faite";
336
        }
337
        else { # on change le message d'erreur fourni par is_table()
338
            $rmsg = "$request->{destination} existe déjà !"; 
339
        }
340
    }
341
    $to_gtalksms_queue->enqueue([$request->{phone}, $rmsg]);
342
}
343

            
344
sub insert {
345
    my $request = shift;
346
    my $rmsg = "";
347
    if (authorized_on_table(table => $request->{table}, id => $request->{phone}, writeable => 1, error => \$rmsg)) {
348
        my $contact_number = 0;
349
        foreach my $contact (split("\n", $request->{body})) {
350
            my ($firstname, $lastname, $phone, $gender) = split(";", $contact);
351
            sql_request("INSERT INTO $request->{table} (firstname,lastname,phone,gender) values ($firstname, $lastname, $phone, $gender)");
352
            $contact_number++;
353
        }
354
        $rmsg = "$contact_number contacts ajoutés dans $request->{table}"
355
    }
356
    $to_gtalksms_queue->enqueue([$request->{phone}, $rmsg]);
357
}
358

            
359
sub react_on_message {
360
    my ($hashref , $request) = @_;
361
    for my $regex (keys(%$hashref)) {
362
        if ($request->{body} =~ m/$regex/i) {
363
            $request->{body} =~ s/$regex//i;
364
            $hashref->{$regex}();
365
            return 1; # true
366
        }
367
    }
368
    return 0; # false
369
}
370

            
371
sub messageCB {
372
    my ($sid, $msg) = @_;
373
    $from_gtalksms_queue->enqueue($msg) if $msg->DefinedBody();
374
}
375

            
376
sub from_gtalksms_parse {
377
    while (my $msg = $from_gtalksms_queue->dequeue) {
378
        my %request = (body => $msg->GetBody);
379
        my %part_from_gtalksms = (
380
            '^Message\s+de\s+\+33([67]\d{8})\s+:\W*' => sub { # Message de +33612345678 : 
381
                $request{phone} = "0" . $1;
382
                $request{phone_owner} = '';
383
                my @results = sql_request("SELECT * FROM section WHERE phone = '$request{phone}'");
384
                my $number_of_candidates = @results;
385
                $number_of_candidates == 0 and return;
386
                $request{phone_owner} .= '(';
387
                foreach (@results) {
388
                    $request{phone_owner} .= "$_->{firstname} $_->{lastname}";
389
                    --$number_of_candidates > 0 and $request{phone_owner} .= ' ou ';
390
                } 
391
                $request{phone_owner} .= ')';
392
                log_bot("message de $request{phone} $request{phone_owner}");
393
            },
394
            '^SMS "(.+)" pour (0\d{9}) délivré\.$' => sub { # SMS "un contenu de SMS" pour 0612345678 délivré.
395
                log_bot("message $1 délivré pour $2");
396
# TODO cond_signal($next_sms);
397
            },
398
            '^Le destinataire par défaut est (\+33|0)\d{9}$' => sub { # Le destinataire par défaut est 0612345678: inutile
399
                return;
400
            },
401
        );
402
        my %part_from_user = (
403
            '^stoppe le bousin$' => sub {
404
                log_bot("arrêt demandé par $request{phone} $request{phone_owner}");
405
                $bot->Disconnect();
406
            },
407
            '^\s*envoi\s+[aà]\s+(\w+)\s*:\W*' => sub {
408
                $request{table} = lc $1;
409
                &massive_send_sms(\%request);
410
            },
411
            '^\s*sms\s+pour\s+(0\d{9})\s*:(.+)$' => sub {
412
                $to_gtalksms_queue->enqueue([$1, $2]);
413
            },
414
            '^\s*copie\s+(\w+)\s+en\s+(\w+)(.*)$' => sub {
415
                $request{origin} = lc $1;
416
                $request{destination} = lc $2;
417
                $request{comment} = $3 =~ s/^\s*//r;
418
                &copy(\%request);
419
            },
420
            '^\s*ajoute\s+dans\+(\w+)\s*:\n' => sub {
421
                $request{table} = lc $1;
422
                &insert(\%request);
423
            },
424
            '^\s*ping\s*$' => sub {
425
                log_bot("envoi d'un pong à $request{phone} $request{phone_owner}");
426
                $to_gtalksms_queue->enqueue([$request{phone}, "pong ($maxs_status)"]);
427
            },
428
        );
429

            
430
        if (react_on_message(\%part_from_gtalksms, \%request)) {
431
            if (defined $request{phone} and !(is_authorized($request{phone}) and react_on_message(\%part_from_user, \%request))) {
432
                $request{subject} = "SMS recu de $request{phone} $request{phone_owner}";
433
                $mail_queue->enqueue(\%request);
434
            }
435
        }
436
        else {
437
            log_bot("message de type inconnu: $request{body}") unless $request{body} eq "";
438
        }
439

            
440
        undef %request;
441
    }
442
}
443

            
444
$from_gtalksms->detach; # gère la file des événements produits par GTalkSMS
445
$to_gtalksms->detach;   # gère la file des envois de SMS par GTalkSMS
446
$mail->detach;          # gère la file des mails
447
$bot->SetMessageCallBacks(chat => \&messageCB);
448

            
449
#print "Logged in to $hostName:$portNumber...\n";
450
$bot->PresenceSend();
451
my $roster = $bot->Roster;
452
$roster->add($cfg::config{xmpp}->{maxs});
453

            
454
while(defined($bot->Process())) { 
455
    $bot->RosterGet();
456
    my $status = $roster->query($cfg::config{xmpp}->{maxs},'resources');
457
    $maxs_status = $status->{GTalkSMS}->{status} =~ s/^GTalkSMS - //r if $status;
458
}
459
print "fin";