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');
60 my %skip = (); #used only by taqua
67 'extra_sql' => 'WHERE freesidestatus IS NULL '.
68 ' AND freesiderewritestatus IS NULL '.
70 ' LIMIT 1024 FOR UPDATE', #arbitrary, but don't eat too much memory
74 next if $skip{$cdr->acctid}; #used only by taqua
79 if ($conf->exists('cdr-skip_duplicate_rewrite')) {
80 #qsearch can't handle timestamp type of calldate
81 my $sth = dbh->prepare(
82 'SELECT 1 FROM cdr WHERE src=? AND dst=? AND calldate=? AND acctid < ? LIMIT 1'
84 $sth->execute($cdr->src,$cdr->dst,$cdr->calldate,$cdr->acctid) or die $sth->errstr;
85 my $isdup = $sth->fetchrow_hashref;
88 #we only act on this cdr, not touching previous dupes
89 #if a dupe somehow creeped in previously, too late to fix it
90 $cdr->freesidestatus('skipped'); #prevent it from being billed
91 push(@status,'duplicate');
95 if ( $conf->exists('cdr-asterisk_forward_rewrite')
96 && $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst
102 warn "dst ". $cdr->dst. " does not match dstchannel $dst ".
103 "(". $cdr->dstchannel. "); rewriting CDR as a forwarded call";
105 $cdr->charged_party($cdr->dst);
109 push @status, 'asterisk_forward';
113 # XXX weird special case stuff--can we modularize this somehow?
115 if ( $conf->exists('cdr-asterisk_australia_rewrite') and
116 $cdr->disposition eq 'ANSWERED' ) {
119 if ( $dst =~ /^0?(12|13|1800|1900|0055)/ ) {
120 # toll free or smart numbers, any length
122 $cdr->charged_party($dst);
124 elsif ( $dst =~ /^(11|0011)/ ) {
125 # will be followed by country code
126 $type = 'international';
127 $dst =~ s/^$1/0011/; #standardize
130 elsif ( length($dst) == 10 and$dst =~ /^04/ ) {
133 elsif ( length($dst) == 10 and $dst =~ /^02|03|07|08/ ) {
136 elsif ( length($dst) == 8 ) {
137 # local call, no area code
143 if ( $type and exists($cdr_type{$type}) ) {
144 $cdr->cdrtypenum($cdr_type{$type});
145 push @status, 'asterisk_australia';
148 $warning{"no CDR type defined for $type calls"}++;
152 if ( $conf->exists('cdr-charged_party_rewrite') && ! $cdr->charged_party ) {
154 $cdr->set_charged_party;
155 push @status, 'charged_party';
159 if ( $cdr->cdrtypenum == 1
162 $conf->exists('cdr-taqua-accountcode_rewrite') or
163 $conf->exists('cdr-taqua-callerid_rewrite') )
167 #find the matching CDR
168 my %search = ( 'sessionnum' => $cdr->sessionnum );
169 if ( $cdr->lastapp eq 'acctcode' ) {
170 $search{'src'} = $cdr->subscriber;
171 } elsif ( $cdr->lastapp eq 'CallerId' ) {
172 $search{'dst'} = $cdr->subscriber;
174 my $primary = qsearchs('cdr', \%search);
176 unless ( $primary ) {
178 my $cantfind = "can't find primary CDR with session ". $cdr->sessionnum.
179 ", src ". $cdr->subscriber;
180 if ( $cdr->calldate_unix + $sessionnum_giveup < time ) {
181 warn "ERROR: $cantfind; giving up\n";
182 push @status, 'taqua-sessionnum-NOTFOUND';
183 $cdr->status('done'); #so it doesn't try to rate
184 delete $sessionnum_unmatch{$cdr->acctid}; #so it doesn't suck mem
186 warn "WARNING: $cantfind; will keep trying\n";
187 $sessionnum_unmatch{$cdr->acctid} = time;
193 if ( $cdr->lastapp eq 'acctcode' ) {
194 # lastdata contains the dialed account code
195 $primary->accountcode( $cdr->lastdata );
196 push @status, 'taqua-accountcode';
197 } elsif ( $cdr->lastapp eq 'CallerId' ) {
198 # lastdata contains "allowed" or "restricted"
199 # or case variants thereof
200 if ( lc($cdr->lastdata) eq 'restricted' ) {
201 $primary->clid( 'PRIVATE' );
203 push @status, 'taqua-callerid';
205 warn "unknown Taqua service name: ".$cdr->lastapp."\n";
207 #$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
208 my $error = $primary->replace if $primary->modified;
210 warn "WARNING: error rewriting primary CDR (will retry): $error\n";
213 $skip{$primary->acctid} = 1;
215 $cdr->status('done'); #so it doesn't try to rate
221 if ( $conf->exists('cdr-userfield_dnis_rewrite') and
222 $cdr->userfield =~ /DNIS=(\d+)/ ) {
224 push @status, 'userfield_dnis';
227 if ( $conf->exists('cdr-intl_to_domestic_rewrite') and
228 $cdr->dst =~ /^(011)(\d{0,7})$/ ) {
230 push @status, 'intl_to_domestic';
233 $cdr->freesiderewritestatus(
234 scalar(@status) ? join('/', @status) : 'skipped'
237 my $error = $cdr->replace;
240 warn "WARNING: error rewriting CDR (will retry in 30 seconds):".
242 sleep 30; #i dunno, wait and see if the database comes back?
245 last if sigterm() || sigint();
249 foreach (sort keys %warning) {
250 warn "WARNING: $_ (x $warning{$_})\n";
254 myexit() if sigterm() || sigint();
255 #sleep 1 unless $found;
256 sleep 5 unless $found;
263 $conf->exists('cdr-asterisk_forward_rewrite')
264 || $conf->exists('cdr-asterisk_australia_rewrite')
265 || $conf->exists('cdr-charged_party_rewrite')
266 || $conf->exists('cdr-taqua-accountcode_rewrite')
267 || $conf->exists('cdr-taqua-callerid_rewrite')
268 || $conf->exists('cdr-intl_to_domestic_rewrite')
269 || $conf->exists('cdr-userfield_dnis_rewrite')
270 || $conf->exists('cdr-skip_duplicate_rewrite')
276 die "Usage:\n\n freeside-cdrrewrited user\n";
281 freeside-cdrrewrited - Real-time daemon for CDR rewriting
289 Runs continuously, searches for CDRs and does forwarded-call rewriting if any
290 of the following config options are enabled:
294 =item cdr-skip_duplicate_rewrite
296 Marks as 'skipped' (prevents billing for) any CDRs with
297 a src, dst and calldate identical to an existing CDR
299 =item cdr-asterisk_australia_rewrite
301 Classifies Australian numbers as domestic, mobile, tollfree, international, or
302 "other", and tries to assign a cdrtypenum based on that.
304 =item cdr-asterisk_forward_rewrite
306 Identifies Asterisk forwarded calls using the 'dstchannel' field. If the
307 dstchannel is "Local/" followed by a number, but the number doesn't match the
308 dst field, the dst field will be rewritten to match.
310 =item cdr-charged_party_rewrite
312 Calls set_charged_party on all calls.
314 =item cdr-taqua-accountcode_rewrite
316 =item cdr-taqua-callerid_rewrite
318 These actually have the same effect. Taqua uses cdrtypenum = 1 to tag accessory
319 records. They will have "sessionnum" = that of the primary record, and
320 "lastapp" indicating their function:
322 - "acctcode": "lastdata" contains the dialed account code. Insert this into the
323 accountcode field of the primary record.
325 - "CallerId": "lastdata" contains "allowed" or "restricted". If "restricted"
326 then the clid field of the primary record is set to "PRIVATE".
328 =item cdr-intl_to_domestic_rewrite
330 Finds records where the destination number has the "011" international prefix,
331 but with seven or fewer digits in the rest of the number, and strips the "011"
332 prefix so that they will be treated as domestic calls. This is very uncommon.