default to a session cookie instead of setting an explicit timeout, weird timezone...
[freeside.git] / FS / bin / freeside-cdrrewrited
1 #!/usr/bin/perl -w
2
3 use strict;
4 use vars qw( $conf );
5 use FS::Daemon ':all'; #daemonize1 drop_root daemonize2 myexit logfile sig*
6 use FS::UID qw( adminsuidsetup );
7 use FS::Record qw( qsearch qsearchs dbh );
8 #use FS::cdr;
9 #use FS::cust_pkg;
10 #use FS::queue;
11
12 my $user = shift or die &usage;
13
14 #daemonize1('freeside-sprepaidd', $user); #keep unique pid files w/multi installs
15 daemonize1('freeside-cdrrewrited');
16
17 drop_root();
18
19 adminsuidsetup($user);
20
21 logfile( "%%%FREESIDE_LOG%%%/cdrrewrited-log.". $FS::UID::datasrc );
22
23 daemonize2();
24
25 $conf = new FS::Conf;
26
27 die "not running; relevant conf options are all off\n"
28   unless _shouldrun();
29
30 #--
31
32 #used for taqua
33 my %sessionnum_unmatch = ();
34 my $sessionnum_retry = 4 * 60 * 60; # 4 hours
35 my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days
36
37 my %cdr_type = map { lc($_->cdrtypename) => $_->cdrtypenum } 
38   qsearch('cdr_type',{});
39
40 while (1) {
41
42   #hmm... don't want to do an expensive search with an ever-growing bunch
43   # of unprocessed CDRs during the month... better to mark them all as
44   # rewritten "skipped", i.e. why we're a daemon in the first place
45   # instead of just doing this search like normal CDRs
46
47   #hmm :/
48   #used only by taqua, should have no effect otherwise
49   my @recent = grep { ($sessionnum_unmatch{$_} + $sessionnum_retry) > time }
50                  keys %sessionnum_unmatch;
51   my $extra_sql = scalar(@recent)
52                     ? ' AND acctid NOT IN ('. join(',', @recent). ') '
53                     : '';
54
55   #order matters for removing dupes--only the first is preserved
56   $extra_sql .= ' ORDER BY acctid '
57     if $conf->exists('cdr-skip_duplicate_rewrite')
58     || $conf->exists('cdr-skip_duplicate_rewrite-sipcallid');
59
60   my $found = 0;
61   my %skip = (); #used only by taqua
62   my %warning = ();
63
64   foreach my $cdr ( 
65     qsearch( {
66       'table'     => 'cdr',
67       'hashref'   => {},
68       'extra_sql' => 'WHERE freesidestatus IS NULL '.
69                      ' AND freesiderewritestatus IS NULL '.
70                      $extra_sql.
71                      ' LIMIT 1024 FOR UPDATE', #arbitrary, but don't eat too much memory
72     } )
73   ) {
74
75     next if $skip{$cdr->acctid}; #used only by taqua
76
77     $found = 1;
78     my @status = ();
79
80     if ($conf->exists('cdr-skip_duplicate_rewrite')) {
81       #qsearch can't handle timestamp type of calldate
82       my $sth = dbh->prepare(
83         'SELECT 1 FROM cdr WHERE src=? AND dst=? AND calldate=? AND acctid < ? LIMIT 1'
84       ) or die dbh->errstr;
85       $sth->execute($cdr->src,$cdr->dst,$cdr->calldate,$cdr->acctid) or die $sth->errstr;
86       my $isdup = $sth->fetchrow_hashref;
87       $sth->finish;
88       if ($isdup) {
89         #we only act on this cdr, not touching previous dupes
90         #if a dupe somehow creeped in previously, too late to fix it
91         $cdr->freesidestatus('skipped'); #prevent it from being billed
92         push(@status,'duplicate');
93       }
94     }
95
96     if ($conf->exists('cdr-skip_duplicate_rewrite-sipcallid')) {
97       my $sth = dbh->prepare(
98         'SELECT 1 FROM cdr WHERE sipcallid=? AND acctid < ? LIMIT 1'
99       ) or die dbh->errstr;
100       $sth->execute($cdr->sipcallid, $cdr->acctid) or die $sth->errstr;
101       my $isdup = $sth->fetchrow_hashref;
102       $sth->finish;
103       if ($isdup) {
104         #we only act on this cdr, not touching previous dupes
105         #if a dupe somehow creeped in previously, too late to fix it
106         $cdr->freesidestatus('skipped'); #prevent it from being billed
107         push(@status,'duplicate');
108       }
109     }
110
111
112     if ( $conf->exists('cdr-asterisk_forward_rewrite')
113          && $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst
114        )
115     {
116
117       my $dst = $1;
118
119       warn "dst ". $cdr->dst. " does not match dstchannel $dst ".
120            "(". $cdr->dstchannel. "); rewriting CDR as a forwarded call";
121
122       $cdr->charged_party($cdr->dst);
123       $cdr->dst($dst);
124       $cdr->amaflags(2);
125
126       push @status, 'asterisk_forward';
127
128     }
129
130     # XXX weird special case stuff--can we modularize this somehow?
131     # reference RT#16271
132     if ( $conf->exists('cdr-asterisk_australia_rewrite') and
133          $cdr->disposition eq 'ANSWERED' ) {
134       my $dst = $cdr->dst;
135       my $type;
136       if ( $dst =~ /^0?(12|13|1800|1900|0055)/ ) {
137         # toll free or smart numbers, any length
138         $type = 'tollfree';
139         $cdr->charged_party($dst);
140       }
141       elsif ( $dst =~ /^(11|0011)/ ) {
142         # will be followed by country code
143         $type = 'international';
144         $dst =~ s/^$1/0011/; #standardize
145         $cdr->dst($dst);
146       }
147       elsif ( length($dst) == 10 and$dst =~ /^04/ ) {
148         $type = 'mobile';
149       }
150       elsif ( length($dst) == 10 and $dst =~ /^02|03|07|08/ ) {
151         $type = 'domestic';
152       }
153       elsif ( length($dst) == 8 ) {
154         # local call, no area code
155         $type = 'domestic';
156       }
157       else {
158         $type = 'other';
159       }
160       if ( $type and exists($cdr_type{$type}) ) {
161         $cdr->cdrtypenum($cdr_type{$type});
162         push @status, 'asterisk_australia';
163       }
164       else {
165         $warning{"no CDR type defined for $type calls"}++;
166       }
167     }
168
169     if ( $conf->exists('cdr-charged_party_rewrite') && ! $cdr->charged_party ) {
170
171       $cdr->set_charged_party;
172       push @status, 'charged_party';
173
174     }
175
176     if (     $cdr->cdrtypenum == 1
177          and $cdr->lastapp
178          and (
179             $conf->exists('cdr-taqua-accountcode_rewrite') or
180             $conf->exists('cdr-taqua-callerid_rewrite') )
181        )
182     {
183
184       #find the matching CDR
185       my %search = ( 'sessionnum' => $cdr->sessionnum );
186       if ( $cdr->lastapp eq 'acctcode' ) {
187         $search{'src'} = $cdr->subscriber;
188       } elsif ( $cdr->lastapp eq 'CallerId' ) {
189         $search{'dst'} = $cdr->subscriber;
190       }
191       my $primary = qsearchs('cdr', \%search);
192
193       unless ( $primary ) {
194
195         my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum.
196                        ", src ". $cdr->subscriber;
197         if ( $cdr->calldate_unix + $sessionnum_giveup < time ) {
198           warn "ERROR: $cantfind; giving up\n";
199           push @status, 'taqua-sessionnum-NOTFOUND';
200           $cdr->status('done'); #so it doesn't try to rate
201           delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem
202         } else {
203           warn "WARNING: $cantfind; will keep trying\n";
204           $sessionnum_unmatch{$cdr->acctid} = time;
205           next;
206         }
207
208       } else {
209
210         if ( $cdr->lastapp eq 'acctcode' ) {
211           # lastdata contains the dialed account code
212           $primary->accountcode( $cdr->lastdata );
213           push @status, 'taqua-accountcode';
214         } elsif ( $cdr->lastapp eq 'CallerId' ) {
215           # lastdata contains "allowed" or "restricted"
216           # or case variants thereof
217           if ( lc($cdr->lastdata) eq 'restricted' ) {
218             $primary->clid( 'PRIVATE' );
219           }
220           push @status, 'taqua-callerid';
221         } else {
222           warn "unknown Taqua service name: ".$cdr->lastapp."\n";
223         }
224         #$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
225         my $error = $primary->replace if $primary->modified;
226         if ( $error ) {
227           warn "WARNING: error rewriting primary CDR (will retry): $error\n";
228           next;
229         }
230         $skip{$primary->acctid} = 1;
231
232         $cdr->status('done'); #so it doesn't try to rate
233
234       }
235
236     }
237
238     if ( $conf->exists('cdr-userfield_dnis_rewrite') and
239          $cdr->userfield =~ /DNIS=(\d+)/ ) {
240       $cdr->dst($1);
241       push @status, 'userfield_dnis';
242     }
243
244     if ( $conf->exists('cdr-intl_to_domestic_rewrite') and
245          $cdr->dst =~ /^(011)(\d{0,7})$/ ) {
246       $cdr->dst($2);
247       push @status, 'intl_to_domestic';
248     }
249
250     $cdr->freesiderewritestatus(
251       scalar(@status) ? join('/', @status) : 'skipped'
252     );
253
254     my $error = $cdr->replace;
255
256     if ( $error ) {
257       warn "WARNING: error rewriting CDR (will retry in 30 seconds):".
258            " $error\n";
259       sleep 30; #i dunno, wait and see if the database comes back?
260     }
261
262     last if sigterm() || sigint();
263
264   }
265
266   foreach (sort keys %warning) {
267     warn "WARNING: $_ (x $warning{$_})\n";
268   }
269   %warning = ();
270
271   myexit() if sigterm() || sigint();
272   #sleep 1 unless $found;
273   sleep 5 unless $found;
274
275 }
276
277 #--
278
279 sub _shouldrun {
280      $conf->exists('cdr-asterisk_forward_rewrite')
281   || $conf->exists('cdr-asterisk_australia_rewrite')
282   || $conf->exists('cdr-charged_party_rewrite')
283   || $conf->exists('cdr-taqua-accountcode_rewrite')
284   || $conf->exists('cdr-taqua-callerid_rewrite')
285   || $conf->exists('cdr-intl_to_domestic_rewrite')
286   || $conf->exists('cdr-userfield_dnis_rewrite')
287   || $conf->exists('cdr-skip_duplicate_rewrite')
288   || $conf->exists('cdr-skip_duplicate_rewrite-sipcallid')
289   || 0
290   ;
291 }
292
293 sub usage { 
294   die "Usage:\n\n  freeside-cdrrewrited user\n";
295 }
296
297 =head1 NAME
298
299 freeside-cdrrewrited - Real-time daemon for CDR rewriting
300
301 =head1 SYNOPSIS
302
303   freeside-cdrrewrited
304
305 =head1 DESCRIPTION
306
307 Runs continuously, searches for CDRs and does forwarded-call rewriting if any
308 of the following config options are enabled:
309
310 =over 4
311
312 =item cdr-skip_duplicate_rewrite
313
314 Marks as 'skipped' (prevents billing for) any CDRs with 
315 a src, dst and calldate identical to an existing CDR
316
317 =item cdr-skip_duplicate_rewrite-sipcallid
318
319 Marks as 'skipped' (prevents billing for) any CDRs with 
320 a sipcallid identical to an existing CDR
321
322 =item cdr-asterisk_australia_rewrite
323
324 Classifies Australian numbers as domestic, mobile, tollfree, international, or
325 "other", and tries to assign a cdrtypenum based on that.
326
327 =item cdr-asterisk_forward_rewrite
328
329 Identifies Asterisk forwarded calls using the 'dstchannel' field. If the
330 dstchannel is "Local/" followed by a number, but the number doesn't match the
331 dst field, the dst field will be rewritten to match.
332
333 =item cdr-charged_party_rewrite
334
335 Calls set_charged_party on all calls.
336
337 =item cdr-taqua-accountcode_rewrite
338
339 =item cdr-taqua-callerid_rewrite
340
341 These actually have the same effect. Taqua uses cdrtypenum = 1 to tag accessory
342 records. They will have "sessionnum" = that of the primary record, and
343 "lastapp" indicating their function:
344
345 - "acctcode": "lastdata" contains the dialed account code. Insert this into the
346 accountcode field of the primary record.
347
348 - "CallerId": "lastdata" contains "allowed" or "restricted". If "restricted"
349 then the clid field of the primary record is set to "PRIVATE".
350
351 =item cdr-intl_to_domestic_rewrite
352
353 Finds records where the destination number has the "011" international prefix,
354 but with seven or fewer digits in the rest of the number, and strips the "011"
355 prefix so that they will be treated as domestic calls. This is very uncommon.
356
357 =head1 SEE ALSO
358
359 =cut
360
361 1;