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
68 'extra_sql' => 'WHERE freesidestatus IS NULL '.
69 ' AND freesiderewritestatus IS NULL '.
71 ' LIMIT 1024 FOR UPDATE', #arbitrary, but don't eat too much memory
75 next if $skip{$cdr->acctid}; #used only by taqua
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'
85 $sth->execute($cdr->src,$cdr->dst,$cdr->calldate,$cdr->acctid) or die $sth->errstr;
86 my $isdup = $sth->fetchrow_hashref;
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');
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'
100 $sth->execute($cdr->sipcallid, $cdr->acctid) or die $sth->errstr;
101 my $isdup = $sth->fetchrow_hashref;
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');
112 if ( $conf->exists('cdr-asterisk_forward_rewrite')
113 && $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst
119 warn "dst ". $cdr->dst. " does not match dstchannel $dst ".
120 "(". $cdr->dstchannel. "); rewriting CDR as a forwarded call";
122 $cdr->charged_party($cdr->dst);
126 push @status, 'asterisk_forward';
130 # XXX weird special case stuff--can we modularize this somehow?
132 if ( $conf->exists('cdr-asterisk_australia_rewrite') and
133 $cdr->disposition eq 'ANSWERED' ) {
136 if ( $dst =~ /^0?(12|13|1800|1900|0055)/ ) {
137 # toll free or smart numbers, any length
139 $cdr->charged_party($dst);
141 elsif ( $dst =~ /^(11|0011)/ ) {
142 # will be followed by country code
143 $type = 'international';
144 $dst =~ s/^$1/0011/; #standardize
147 elsif ( length($dst) == 10 and$dst =~ /^04/ ) {
150 elsif ( length($dst) == 10 and $dst =~ /^02|03|07|08/ ) {
153 elsif ( length($dst) == 8 ) {
154 # local call, no area code
160 if ( $type and exists($cdr_type{$type}) ) {
161 $cdr->cdrtypenum($cdr_type{$type});
162 push @status, 'asterisk_australia';
165 $warning{"no CDR type defined for $type calls"}++;
169 if ( $conf->exists('cdr-charged_party_rewrite') && ! $cdr->charged_party ) {
171 $cdr->set_charged_party;
172 push @status, 'charged_party';
176 if ( $cdr->cdrtypenum == 1
179 $conf->exists('cdr-taqua-accountcode_rewrite') or
180 $conf->exists('cdr-taqua-callerid_rewrite') )
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;
191 my $primary = qsearchs('cdr', \%search);
193 unless ( $primary ) {
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
203 warn "WARNING: $cantfind; will keep trying\n";
204 $sessionnum_unmatch{$cdr->acctid} = time;
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' );
220 push @status, 'taqua-callerid';
222 warn "unknown Taqua service name: ".$cdr->lastapp."\n";
224 #$primary->freesiderewritestatus( 'taqua-accountcode-primary' );
225 my $error = $primary->replace if $primary->modified;
227 warn "WARNING: error rewriting primary CDR (will retry): $error\n";
230 $skip{$primary->acctid} = 1;
232 $cdr->status('done'); #so it doesn't try to rate
238 if ( $conf->exists('cdr-userfield_dnis_rewrite') and
239 $cdr->userfield =~ /DNIS=(\d+)/ ) {
241 push @status, 'userfield_dnis';
244 if ( $conf->exists('cdr-intl_to_domestic_rewrite') and
245 $cdr->dst =~ /^(011)(\d{0,7})$/ ) {
247 push @status, 'intl_to_domestic';
250 $cdr->freesiderewritestatus(
251 scalar(@status) ? join('/', @status) : 'skipped'
254 my $error = $cdr->replace;
257 warn "WARNING: error rewriting CDR (will retry in 30 seconds):".
259 sleep 30; #i dunno, wait and see if the database comes back?
262 last if sigterm() || sigint();
266 foreach (sort keys %warning) {
267 warn "WARNING: $_ (x $warning{$_})\n";
271 myexit() if sigterm() || sigint();
272 #sleep 1 unless $found;
273 sleep 5 unless $found;
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')
294 die "Usage:\n\n freeside-cdrrewrited user\n";
299 freeside-cdrrewrited - Real-time daemon for CDR rewriting
307 Runs continuously, searches for CDRs and does forwarded-call rewriting if any
308 of the following config options are enabled:
312 =item cdr-skip_duplicate_rewrite
314 Marks as 'skipped' (prevents billing for) any CDRs with
315 a src, dst and calldate identical to an existing CDR
317 =item cdr-skip_duplicate_rewrite-sipcallid
319 Marks as 'skipped' (prevents billing for) any CDRs with
320 a sipcallid identical to an existing CDR
322 =item cdr-asterisk_australia_rewrite
324 Classifies Australian numbers as domestic, mobile, tollfree, international, or
325 "other", and tries to assign a cdrtypenum based on that.
327 =item cdr-asterisk_forward_rewrite
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.
333 =item cdr-charged_party_rewrite
335 Calls set_charged_party on all calls.
337 =item cdr-taqua-accountcode_rewrite
339 =item cdr-taqua-callerid_rewrite
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:
345 - "acctcode": "lastdata" contains the dialed account code. Insert this into the
346 accountcode field of the primary record.
348 - "CallerId": "lastdata" contains "allowed" or "restricted". If "restricted"
349 then the clid field of the primary record is set to "PRIVATE".
351 =item cdr-intl_to_domestic_rewrite
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.