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 );
12 my $user = shift or die &usage;
14 #daemonize1('freeside-sprepaidd', $user); #keep unique pid files w/multi installs
15 daemonize1('freeside-cdrrewrited');
19 adminsuidsetup($user);
21 logfile( "%%%FREESIDE_LOG%%%/cdrrewrited-log.". $FS::UID::datasrc );
27 die "not running; relevant conf options are all off\n"
33 my %sessionnum_unmatch = ();
34 my $sessionnum_retry = 4 * 60 * 60; # 4 hours
35 my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days
37 my %cdr_type = map { lc($_->cdrtypename) => $_->cdrtypenum }
38 qsearch('cdr_type',{});
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
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). ') '
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');
61 my %skip = (); #used only by taqua
67 'extra_sql' => 'FOR UPDATE', #XXX overwritten by opt below...would fixing this break anything?
69 'extra_sql' => 'WHERE freesidestatus IS NULL '.
70 ' AND freesiderewritestatus IS NULL '.
72 ' LIMIT 1024', #arbitrary, but don't eat too much memory
76 next if $skip{$cdr->acctid}; #used only by taqua
81 if ($conf->exists('cdr-skip_duplicate_rewrite')) {
82 #qsearch can't handle timestamp type of calldate
83 my $sth = dbh->prepare(
84 'SELECT 1 FROM cdr WHERE src=? AND dst=? AND calldate=? AND acctid < ? LIMIT 1'
86 $sth->execute($cdr->src,$cdr->dst,$cdr->calldate,$cdr->acctid) or die $sth->errstr;
87 my $isdup = $sth->fetchrow_hashref;
90 #we only act on this cdr, not touching previous dupes
91 #if a dupe somehow creeped in previously, too late to fix it
92 $cdr->freesidestatus('skipped'); #prevent it from being billed
93 push(@status,'duplicate');
97 if ($conf->exists('cdr-skip_duplicate_rewrite-sipcallid')) {
98 my $sth = dbh->prepare(
99 'SELECT 1 FROM cdr WHERE sipcallid=? AND acctid < ? LIMIT 1'
100 ) or die dbh->errstr;
101 $sth->execute($cdr->sipcallid, $cdr->acctid) or die $sth->errstr;
102 my $isdup = $sth->fetchrow_hashref;
105 #we only act on this cdr, not touching previous dupes
106 #if a dupe somehow creeped in previously, too late to fix it
107 $cdr->freesidestatus('skipped'); #prevent it from being billed
108 push(@status,'duplicate');
113 if ( $conf->exists('cdr-asterisk_forward_rewrite')
114 && $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst
120 warn "dst ". $cdr->dst. " does not match dstchannel $dst ".
121 "(". $cdr->dstchannel. "); rewriting CDR as a forwarded call";
123 $cdr->charged_party($cdr->dst);
127 push @status, 'asterisk_forward';
131 # XXX weird special case stuff--can we modularize this somehow?
133 if ( $conf->exists('cdr-asterisk_australia_rewrite') and
134 $cdr->disposition eq 'ANSWERED' ) {
137 if ( $dst =~ /^0?(12|13|1800|1900|0055)/ ) {
138 # toll free or smart numbers, any length
140 $cdr->charged_party($dst);
142 elsif ( $dst =~ /^(11|0011)/ ) {
143 # will be followed by country code
144 $type = 'international';
145 $dst =~ s/^$1/0011/; #standardize
148 elsif ( length($dst) == 10 and$dst =~ /^04/ ) {
151 elsif ( length($dst) == 10 and $dst =~ /^02|03|07|08/ ) {
154 elsif ( length($dst) == 8 ) {
155 # local call, no area code
161 if ( $type and exists($cdr_type{$type}) ) {
162 $cdr->cdrtypenum($cdr_type{$type});
163 push @status, 'asterisk_australia';
166 $warning{"no CDR type defined for $type calls"}++;
170 if ( $conf->exists('cdr-charged_party_rewrite') && ! $cdr->charged_party ) {
172 $cdr->set_charged_party;
173 push @status, 'charged_party';
177 if ( $cdr->cdrtypenum == 1
180 $conf->exists('cdr-taqua-accountcode_rewrite') or
181 $conf->exists('cdr-taqua-callerid_rewrite') )
185 #find the matching CDR
186 my %search = ( 'sessionnum' => $cdr->sessionnum );
187 if ( $cdr->lastapp eq 'acctcode' ) {
188 $search{'src'} = $cdr->subscriber;
189 } elsif ( $cdr->lastapp eq 'CallerId' ) {
190 $search{'dst'} = $cdr->subscriber;
192 my $primary = qsearchs('cdr', \%search);
194 unless ( $primary ) {
196 my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum.
197 ", src ". $cdr->subscriber;
198 if ( $cdr->calldate_unix + $sessionnum_giveup < time ) {
199 warn "ERROR: $cantfind; giving up\n";
200 push @status, 'taqua-sessionnum-NOTFOUND';
201 $cdr->status('done'); #so it doesn't try to rate
202 delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem
204 warn "WARNING: $cantfind; will keep trying\n";
205 $sessionnum_unmatch{$cdr->acctid} = time;
211 if ( $cdr->lastapp eq 'acctcode' ) {
212 # lastdata contains the dialed account code
213 $primary->accountcode( $cdr->lastdata );
214 push @status, 'taqua-accountcode';
215 } elsif ( $cdr->lastapp eq 'CallerId' ) {
216 # lastdata contains "allowed" or "restricted"
217 # or case variants thereof
218 if ( lc($cdr->lastdata) eq 'restricted' ) {
219 $primary->clid( 'PRIVATE' );
221 push @status, 'taqua-callerid';
223 warn "unknown Taqua service name: ".$cdr->lastapp."\n";
225 #$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
226 my $error = $primary->replace if $primary->modified;
228 warn "WARNING: error rewriting primary CDR (will retry): $error\n";
231 $skip{$primary->acctid} = 1;
233 $cdr->status('done'); #so it doesn't try to rate
239 if ( $conf->exists('cdr-userfield_dnis_rewrite') and
240 $cdr->userfield =~ /DNIS=(\d+)/ ) {
242 push @status, 'userfield_dnis';
245 if ( $conf->exists('cdr-intl_to_domestic_rewrite') and
246 $cdr->dst =~ /^(011)(\d{0,7})$/ ) {
248 push @status, 'intl_to_domestic';
251 $cdr->freesiderewritestatus(
252 scalar(@status) ? join('/', @status) : 'skipped'
255 my $error = $cdr->replace;
258 warn "WARNING: error rewriting CDR (will retry in 30 seconds):".
260 sleep 30; #i dunno, wait and see if the database comes back?
263 last if sigterm() || sigint();
267 foreach (sort keys %warning) {
268 warn "WARNING: $_ (x $warning{$_})\n";
272 myexit() if sigterm() || sigint();
273 #sleep 1 unless $found;
274 sleep 5 unless $found;
281 $conf->exists('cdr-asterisk_forward_rewrite')
282 || $conf->exists('cdr-asterisk_australia_rewrite')
283 || $conf->exists('cdr-charged_party_rewrite')
284 || $conf->exists('cdr-taqua-accountcode_rewrite')
285 || $conf->exists('cdr-taqua-callerid_rewrite')
286 || $conf->exists('cdr-intl_to_domestic_rewrite')
287 || $conf->exists('cdr-userfield_dnis_rewrite')
288 || $conf->exists('cdr-skip_duplicate_rewrite')
289 || $conf->exists('cdr-skip_duplicate_rewrite-sipcallid')
295 die "Usage:\n\n freeside-cdrrewrited user\n";
300 freeside-cdrrewrited - Real-time daemon for CDR rewriting
308 Runs continuously, searches for CDRs and does forwarded-call rewriting if any
309 of the following config options are enabled:
313 =item cdr-skip_duplicate_rewrite
315 Marks as 'skipped' (prevents billing for) any CDRs with
316 a src, dst and calldate identical to an existing CDR
318 =item cdr-skip_duplicate_rewrite-sipcallid
320 Marks as 'skipped' (prevents billing for) any CDRs with
321 a sipcallid identical to an existing CDR
323 =item cdr-asterisk_australia_rewrite
325 Classifies Australian numbers as domestic, mobile, tollfree, international, or
326 "other", and tries to assign a cdrtypenum based on that.
328 =item cdr-asterisk_forward_rewrite
330 Identifies Asterisk forwarded calls using the 'dstchannel' field. If the
331 dstchannel is "Local/" followed by a number, but the number doesn't match the
332 dst field, the dst field will be rewritten to match.
334 =item cdr-charged_party_rewrite
336 Calls set_charged_party on all calls.
338 =item cdr-taqua-accountcode_rewrite
340 =item cdr-taqua-callerid_rewrite
342 These actually have the same effect. Taqua uses cdrtypenum = 1 to tag accessory
343 records. They will have "sessionnum" = that of the primary record, and
344 "lastapp" indicating their function:
346 - "acctcode": "lastdata" contains the dialed account code. Insert this into the
347 accountcode field of the primary record.
349 - "CallerId": "lastdata" contains "allowed" or "restricted". If "restricted"
350 then the clid field of the primary record is set to "PRIVATE".
352 =item cdr-intl_to_domestic_rewrite
354 Finds records where the destination number has the "011" international prefix,
355 but with seven or fewer digits in the rest of the number, and strips the "011"
356 prefix so that they will be treated as domestic calls. This is very uncommon.