Diff -- /var/www/vhosts/3dshawn.com/admin.3dshawn.com/MODS/MetaAdmin/AutoAck.pm

O Operator
Diff

/var/www/vhosts/3dshawn.com/admin.3dshawn.com/MODS/MetaAdmin/AutoAck.pm

added on local at 2026-07-10 14:13:06

Added
+0
lines
Removed
-0
lines
Context
537
unchanged
Blobs
from 0cb9fe0eed3d
to 0cb9fe0eed3d
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
11package MODS::MetaAdmin::AutoAck;
22#======================================================================
33# Meta-Admin -- auto-acknowledge scheduler for Support Inbox.
44#
55# Design:
66# * discover() -- scan all supported sites for eligible threads,
77# match against active rules, insert scheduled fires
88# into auto_ack_queue (with random delay per rule).
99# * fire_ready() -- process queued rows whose fire_at <= NOW(). Re-runs
1010# eligibility checks at fire time (a human may have
1111# replied in the meantime; the customer may have
1212# closed the thread; global kill switch may be off).
1313#
1414# Both intended to be called from auto_ack_tick.pl, running every minute
1515# from /etc/cron.d.
1616#
1717# Ethical guardrail: the templates themselves are enforced-ack-only by
1818# operator discipline (see the /canned.cgi KPI tile). This module just
1919# fires whatever's in the pool -- so keep the pool clean.
2020#======================================================================
2121use strict;
2222use warnings;
2323use POSIX ();
2424use MODS::DBConnect;
2525use MODS::MetaAdmin::SiteConn;
2626use MODS::MetaAdmin::SupportInbox;
2727use MODS::MetaAdmin::CannedReplies;
2828
2929sub new {
3030 my ($class) = @_;
3131 return bless({
3232 db => MODS::DBConnect->new,
3333 sc => MODS::MetaAdmin::SiteConn->new,
3434 inbox => MODS::MetaAdmin::SupportInbox->new,
3535 canned => MODS::MetaAdmin::CannedReplies->new,
3636 log => [],
3737 }, $class);
3838}
3939
4040sub log_lines { return $_[0]->{log} }
4141
4242sub _log { push @{$_[0]->{log}}, $_[1] }
4343
4444#---------------------------------------------------------------------
4545# is_globally_enabled -> 0/1. The single kill switch.
4646#---------------------------------------------------------------------
4747sub is_globally_enabled {
4848 my $self = shift;
4949 my $dbh = $self->{db}->db_connect or return 0;
5050 my $r = $self->{db}->db_readwrite($dbh,
5151 "SELECT setting_value AS v FROM auto_ack_settings WHERE setting_key='global_enabled' LIMIT 1",
5252 __FILE__, __LINE__);
5353 $self->{db}->db_disconnect($dbh);
5454 return ($r && $r->{v} && $r->{v} eq '1') ? 1 : 0;
5555}
5656
5757sub set_global_enabled {
5858 my ($self, $v) = @_;
5959 $v = $v ? '1' : '0';
6060 my $dbh = $self->{db}->db_connect or return 0;
6161 $self->{db}->db_readwrite($dbh, qq~
6262 UPDATE auto_ack_settings SET setting_value='$v'
6363 WHERE setting_key='global_enabled'
6464 ~, __FILE__, __LINE__);
6565 $self->{db}->db_disconnect($dbh);
6666 return 1;
6767}
6868
6969#---------------------------------------------------------------------
7070# is_business_hours($rule) -> 0/1
7171# Compares current time (Mountain Time) against rule's start/end hours.
7272#---------------------------------------------------------------------
7373sub is_business_hours {
7474 my ($self, $rule) = @_;
7575 return 1 unless $rule->{business_hours_only};
7676 local $ENV{TZ} = 'America/Denver';
7777 POSIX::tzset();
7878 my @lt = localtime(time);
7979 my $hour = $lt[2];
8080 my $s = int($rule->{business_hours_start} || 0);
8181 my $e = int($rule->{business_hours_end} || 23);
8282 if ($s <= $e) { return ($hour >= $s && $hour <= $e) ? 1 : 0; }
8383 # Wrap-around window (e.g. 22..6)
8484 return ($hour >= $s || $hour <= $e) ? 1 : 0;
8585}
8686
8787#---------------------------------------------------------------------
8888# list_rules({ active_only }) -> arrayref of rule hashrefs
8989#---------------------------------------------------------------------
9090sub list_rules {
9191 my ($self, %a) = @_;
9292 my $active = exists $a{active_only} ? $a{active_only} : 0;
9393 my $where = $active ? 'WHERE is_active=1' : '';
9494 my $dbh = $self->{db}->db_connect or return [];
9595 my @rows = $self->{db}->db_readwrite_multiple($dbh, qq~
9696 SELECT * FROM auto_ack_rules $where ORDER BY priority, id
9797 ~, __FILE__, __LINE__);
9898 $self->{db}->db_disconnect($dbh);
9999 return \@rows;
100100}
101101
102102sub get_rule {
103103 my ($self, $id) = @_;
104104 $id = int($id || 0); return undef unless $id > 0;
105105 my $dbh = $self->{db}->db_connect or return undef;
106106 my $r = $self->{db}->db_readwrite($dbh,
107107 "SELECT * FROM auto_ack_rules WHERE id=$id LIMIT 1", __FILE__, __LINE__);
108108 $self->{db}->db_disconnect($dbh);
109109 return $r;
110110}
111111
112112sub save_rule {
113113 my ($self, %a) = @_;
114114 my $id = int($a{id} || 0);
115115 my $dbh = $self->{db}->db_connect or return undef;
116116 my $name_q = $dbh->quote($a{name} // '');
117117 my $desc_q = $dbh->quote($a{description} // '');
118118 my $ss_q = $dbh->quote($a{site_scope} // 'all');
119119 my $tpl = int($a{template_id} || 0);
120120 my $mn = int($a{min_delay_seconds} || 120);
121121 my $mx = int($a{max_delay_seconds} || 480);
122122 my $mno = ($a{match_new_only}) ? 1 : 0;
123123 my $mnar = ($a{match_no_admin_reply}) ? 1 : 0;
124124 my $motmin = int($a{match_customer_other_threads_min} || 0);
125125 my $motmax = int($a{match_customer_other_threads_max} || 999);
126126 my $bho = ($a{business_hours_only}) ? 1 : 0;
127127 my $bhs = int($a{business_hours_start} // 8);
128128 my $bhe = int($a{business_hours_end} // 23);
129129 my $pri = int($a{priority} || 100);
130130 my $act = ($a{is_active} // 1) ? 1 : 0;
131131
132132 if ($id > 0) {
133133 $self->{db}->db_readwrite($dbh, qq~
134134 UPDATE auto_ack_rules SET
135135 name=$name_q, description=$desc_q, template_id=$tpl,
136136 site_scope=$ss_q, min_delay_seconds=$mn, max_delay_seconds=$mx,
137137 match_new_only=$mno, match_no_admin_reply=$mnar,
138138 match_customer_other_threads_min=$motmin,
139139 match_customer_other_threads_max=$motmax,
140140 business_hours_only=$bho, business_hours_start=$bhs,
141141 business_hours_end=$bhe, priority=$pri, is_active=$act
142142 WHERE id=$id LIMIT 1
143143 ~, __FILE__, __LINE__);
144144 } else {
145145 $self->{db}->db_readwrite($dbh, qq~
146146 INSERT INTO auto_ack_rules
147147 (name, description, template_id, site_scope,
148148 min_delay_seconds, max_delay_seconds,
149149 match_new_only, match_no_admin_reply,
150150 match_customer_other_threads_min, match_customer_other_threads_max,
151151 business_hours_only, business_hours_start, business_hours_end,
152152 priority, is_active, created_at)
153153 VALUES
154154 ($name_q, $desc_q, $tpl, $ss_q,
155155 $mn, $mx, $mno, $mnar, $motmin, $motmax,
156156 $bho, $bhs, $bhe, $pri, $act, NOW())
157157 ~, __FILE__, __LINE__);
158158 my $r = $self->{db}->db_readwrite($dbh, "SELECT LAST_INSERT_ID() AS id",
159159 __FILE__, __LINE__);
160160 $id = $r && $r->{id} ? $r->{id} : 0;
161161 }
162162 $self->{db}->db_disconnect($dbh);
163163 return $id;
164164}
165165
166166sub toggle_rule {
167167 my ($self, $id) = @_;
168168 $id = int($id || 0); return 0 unless $id > 0;
169169 my $dbh = $self->{db}->db_connect or return 0;
170170 $self->{db}->db_readwrite($dbh,
171171 "UPDATE auto_ack_rules SET is_active=1-is_active WHERE id=$id LIMIT 1",
172172 __FILE__, __LINE__);
173173 $self->{db}->db_disconnect($dbh);
174174 return 1;
175175}
176176
177177#---------------------------------------------------------------------
178178# discover() -- for each active rule, scan supported sites for eligible
179179# threads, insert into auto_ack_queue. Returns count inserted.
180180#---------------------------------------------------------------------
181181sub discover {
182182 my $self = shift;
183183 return 0 unless $self->is_globally_enabled;
184184
185185 my $rules = $self->list_rules(active_only => 1);
186186 return 0 unless @$rules;
187187
188188 my $supported = $self->{inbox}->supported_slugs;
189189 my %slug_ok = map { $_ => 1 } @$supported;
190190
191191 my $inserted = 0;
192192 foreach my $rule (@$rules) {
193193 # Business hours short-circuit avoids per-site query cost outside window.
194194 next unless $self->is_business_hours($rule);
195195
196196 my @scope_slugs;
197197 if (($rule->{site_scope} || 'all') eq 'all') {
198198 @scope_slugs = @$supported;
199199 } else {
200200 @scope_slugs = grep { $slug_ok{$_} }
201201 split(/\s*,\s*/, $rule->{site_scope});
202202 }
203203 next unless @scope_slugs;
204204
205205 foreach my $slug (@scope_slugs) {
206206 my $candidates = $self->_find_candidates($slug, $rule);
207207 foreach my $cand (@$candidates) {
208208 my $queued = $self->_enqueue($slug, $cand, $rule);
209209 $inserted++ if $queued;
210210 }
211211 }
212212 }
213213 return $inserted;
214214}
215215
216216#---------------------------------------------------------------------
217217# _find_candidates($slug, $rule) -> arrayref of {thread_id, customer_user_id}
218218#
219219# Runs per-site via SiteConn. Handles PT column names via SupportInbox's
220220# adapter (fetch adapter, use its column names in the SQL).
221221#---------------------------------------------------------------------
222222sub _find_candidates {
223223 my ($self, $slug, $rule) = @_;
224224 # Adapter columns for THIS site
225225 my $adapter = MODS::MetaAdmin::SupportInbox->adapter_for($slug);
226226 return [] unless $adapter;
227227 my $cust_col = $adapter->{thread_customer_col};
228228
229229 # Only look at recent (72h) threads to keep the scan bounded.
230230 # "new_only" = last message from customer/user (not admin) AND thread status
231231 # is one of the "waiting for us" family (native to each site).
232232 my @native_open_statuses;
233233 push @native_open_statuses, $adapter->{status_reverse}{open};
234234 push @native_open_statuses, $adapter->{status_reverse}{waiting_admin};
235235 my %uniq; @native_open_statuses = grep { !$uniq{$_}++ } @native_open_statuses;
236236 my $status_in = join(',', map { "'$_'" } @native_open_statuses);
237237
238238 my $admin_reply_clause = '';
239239 if ($rule->{match_no_admin_reply}) {
240240 $admin_reply_clause = qq~AND NOT EXISTS (
241241 SELECT 1 FROM support_messages m
242242 WHERE m.thread_id = t.id
243243 AND m.$adapter->{message_sender_col} = '$adapter->{admin_sender_value}'
244244 )~;
245245 }
246246
247247 my $sql = qq~
248248 SELECT t.id AS thread_id, t.$cust_col AS customer_user_id
249249 FROM support_threads t
250250 WHERE t.created_at >= DATE_SUB(NOW(), INTERVAL 72 HOUR)
251251 AND t.$adapter->{thread_status_col} IN ($status_in)
252252 $admin_reply_clause
253253 ORDER BY t.id ASC
254254 LIMIT 50
255255 ~;
256256 my $rows = $self->{sc}->query_many($slug, $sql) || [];
257257
258258 # Filter by "other open threads by same customer" bracket + by not-already-queued.
259259 my $dbh = $self->{db}->db_connect;
260260 my @keep;
261261 foreach my $r (@$rows) {
262262 my $tid = int($r->{thread_id} || 0);
263263 my $cust = int($r->{customer_user_id} || 0);
264264
265265 # Dedup: already queued/sent for this (site, thread, template)?
266266 my $already = $self->{db}->db_readwrite($dbh, qq~
267267 SELECT id FROM auto_ack_queue
268268 WHERE site='$slug' AND thread_id=$tid AND template_id=$rule->{template_id}
269269 LIMIT 1
270270 ~, __FILE__, __LINE__);
271271 next if $already && $already->{id};
272272
273273 # Dedup: same template already sent to same customer on this site?
274274 if ($cust) {
275275 my $prev = $self->{db}->db_readwrite($dbh, qq~
276276 SELECT id FROM auto_ack_queue
277277 WHERE site='$slug' AND customer_user_id=$cust
278278 AND template_id=$rule->{template_id}
279279 AND status IN ('sent','queued')
280280 LIMIT 1
281281 ~, __FILE__, __LINE__);
282282 next if $prev && $prev->{id};
283283 }
284284
285285 # Other-open-threads-by-same-customer bracket
286286 if ($cust) {
287287 my $other_count = $self->_count_other_open_threads($slug, $cust, $tid, $adapter);
288288 next if $other_count < ($rule->{match_customer_other_threads_min} || 0);
289289 next if $other_count > ($rule->{match_customer_other_threads_max} || 999);
290290 $r->{other_open} = $other_count;
291291 }
292292
293293 push @keep, $r;
294294 }
295295 $self->{db}->db_disconnect($dbh);
296296 return \@keep;
297297}
298298
299299sub _count_other_open_threads {
300300 my ($self, $slug, $cust, $tid, $adapter) = @_;
301301 my $cust_col = $adapter->{thread_customer_col};
302302 my @open_statuses;
303303 push @open_statuses, $adapter->{status_reverse}{open};
304304 push @open_statuses, $adapter->{status_reverse}{waiting_admin};
305305 push @open_statuses, $adapter->{status_reverse}{waiting_customer};
306306 my %uniq; @open_statuses = grep { !$uniq{$_}++ } @open_statuses;
307307 my $status_in = join(',', map { "'$_'" } @open_statuses);
308308 my $r = $self->{sc}->query_one($slug, qq~
309309 SELECT COUNT(*) AS n FROM support_threads
310310 WHERE $cust_col=$cust AND id != $tid
311311 AND $adapter->{thread_status_col} IN ($status_in)
312312 ~);
313313 return $r ? int($r->{n} || 0) : 0;
314314}
315315
316316sub _enqueue {
317317 my ($self, $slug, $cand, $rule) = @_;
318318 my $tid = int($cand->{thread_id});
319319 my $cust = int($cand->{customer_user_id} || 0);
320320 my $tpl = int($rule->{template_id});
321321 my $rid = int($rule->{id});
322322
323323 # Random delay within rule window
324324 my $mn = int($rule->{min_delay_seconds} || 60);
325325 my $mx = int($rule->{max_delay_seconds} || 300);
326326 $mx = $mn if $mx < $mn;
327327 my $delay = $mn + int(rand($mx - $mn + 1));
328328
329329 my $dbh = $self->{db}->db_connect or return 0;
330330 my $ok = eval {
331331 $self->{db}->db_readwrite($dbh, qq~
332332 INSERT INTO auto_ack_queue
333333 (site, thread_id, customer_user_id, template_id, rule_id,
334334 discovered_at, fire_at, status)
335335 VALUES
336336 ('$slug', $tid, $cust, $tpl, $rid,
337337 NOW(), DATE_ADD(NOW(), INTERVAL $delay SECOND), 'queued')
338338 ~, __FILE__, __LINE__);
339339 1;
340340 };
341341 $self->{db}->db_disconnect($dbh);
342342 if (!$ok) {
343343 $self->_log("enqueue $slug/$tid tpl=$tpl failed: $@");
344344 return 0;
345345 }
346346 $self->_log("queued $slug/$tid tpl=$tpl rule=$rid delay=${delay}s");
347347 return 1;
348348}
349349
350350#---------------------------------------------------------------------
351351# fire_ready() -- process queued rows whose fire_at <= NOW(). Returns
352352# arrayref of results.
353353#---------------------------------------------------------------------
354354sub fire_ready {
355355 my $self = shift;
356356 unless ($self->is_globally_enabled) {
357357 $self->_log("global kill switch is OFF, skipping fire_ready");
358358 return [];
359359 }
360360 my $dbh = $self->{db}->db_connect or return [];
361361 my @due = $self->{db}->db_readwrite_multiple($dbh, qq~
362362 SELECT * FROM auto_ack_queue
363363 WHERE status='queued' AND fire_at <= NOW()
364364 ORDER BY fire_at ASC
365365 LIMIT 20
366366 ~, __FILE__, __LINE__);
367367 $self->{db}->db_disconnect($dbh);
368368
369369 my @results;
370370 foreach my $q (@due) {
371371 push @results, $self->_fire_one($q);
372372 }
373373 return \@results;
374374}
375375
376376sub _fire_one {
377377 my ($self, $q) = @_;
378378 my $qid = int($q->{id});
379379 my $slug = $q->{site};
380380 my $tid = int($q->{thread_id});
381381 my $tpl = int($q->{template_id});
382382 my $rule = $self->get_rule($q->{rule_id});
383383 my $tpl_row = $self->{canned}->get($tpl);
384384
385385 # Re-verify eligibility -- state may have shifted since queueing.
386386 my $verdict = $self->_reverify($q, $rule);
387387 if ($verdict->{skip}) {
388388 $self->_mark_skipped($qid, $verdict->{reason});
389389 $self->_log("skip q=$qid $slug/$tid: $verdict->{reason}");
390390 return { id => $qid, action => 'skipped', reason => $verdict->{reason} };
391391 }
392392 unless ($tpl_row) {
393393 $self->_mark_skipped($qid, 'template_deleted');
394394 return { id => $qid, action => 'skipped', reason => 'template_deleted' };
395395 }
396396 unless ($tpl_row->{is_active}) {
397397 $self->_mark_skipped($qid, 'template_inactive');
398398 return { id => $qid, action => 'skipped', reason => 'template_inactive' };
399399 }
400400
401401 # Expand template body against live thread context
402402 my $thread_res = $self->{inbox}->get_thread($slug, $tid);
403403 unless ($thread_res && $thread_res->{thread}) {
404404 $self->_mark_skipped($qid, 'thread_missing');
405405 return { id => $qid, action => 'skipped', reason => 'thread_missing' };
406406 }
407407 my $ctx = $self->{canned}->thread_context($thread_res->{thread});
408408 my $body = $self->{canned}->expand($tpl_row->{body}, $ctx);
409409
410410 my ($ok, $ret) = $self->{inbox}->post_reply($slug, $tid, $body);
411411 if ($ok) {
412412 $self->_mark_sent($qid, $ret);
413413 $self->_log("SENT q=$qid $slug/$tid tpl=$tpl mid=$ret");
414414 return { id => $qid, action => 'sent', message_id => $ret };
415415 } else {
416416 $self->_mark_failed($qid, $ret);
417417 $self->_log("FAIL q=$qid $slug/$tid: $ret");
418418 return { id => $qid, action => 'failed', error => $ret };
419419 }
420420}
421421
422422#---------------------------------------------------------------------
423423# _reverify -- last-mile guards. Runs immediately before firing.
424424# Returns { skip => 0|1, reason => '...' }.
425425#---------------------------------------------------------------------
426426sub _reverify {
427427 my ($self, $q, $rule) = @_;
428428 return { skip => 1, reason => 'rule_deleted' } unless $rule;
429429 return { skip => 1, reason => 'rule_disabled' } unless $rule->{is_active};
430430 return { skip => 1, reason => 'outside_hours' } unless $self->is_business_hours($rule);
431431
432432 my $slug = $q->{site};
433433 my $tid = int($q->{thread_id});
434434 my $adapter = MODS::MetaAdmin::SupportInbox->adapter_for($slug);
435435 return { skip => 1, reason => 'unsupported_site' } unless $adapter;
436436
437437 # Fetch current thread state
438438 my $t = $self->{sc}->query_one($slug, qq~
439439 SELECT id, $adapter->{thread_status_col} AS status,
440440 $adapter->{thread_customer_col} AS customer_user_id
441441 FROM support_threads WHERE id = $tid LIMIT 1
442442 ~);
443443 return { skip => 1, reason => 'thread_gone' } unless $t;
444444
445445 # If the thread has since been closed/resolved, skip.
446446 my $native_status = $t->{status} || '';
447447 my $normalized = $adapter->{status_map}{$native_status} || $native_status;
448448 if ($normalized eq 'closed' || $normalized eq 'resolved') {
449449 return { skip => 1, reason => 'thread_closed' };
450450 }
451451
452452 # If any admin message has been posted since queueing, skip -- a human
453453 # got there first, don't step on their reply.
454454 if ($rule->{match_no_admin_reply}) {
455455 my $adm = $self->{sc}->query_one($slug, qq~
456456 SELECT COUNT(*) AS n FROM support_messages
457457 WHERE thread_id = $tid
458458 AND $adapter->{message_sender_col} = '$adapter->{admin_sender_value}'
459459 ~);
460460 return { skip => 1, reason => 'admin_replied_first' }
461461 if $adm && $adm->{n};
462462 }
463463
464464 return { skip => 0 };
465465}
466466
467467sub _mark_sent {
468468 my ($self, $qid, $mid) = @_;
469469 $mid = int($mid || 0);
470470 my $dbh = $self->{db}->db_connect or return;
471471 $self->{db}->db_readwrite($dbh, qq~
472472 UPDATE auto_ack_queue
473473 SET status='sent', sent_at=NOW(), message_id=$mid
474474 WHERE id=$qid LIMIT 1
475475 ~, __FILE__, __LINE__);
476476 $self->{db}->db_disconnect($dbh);
477477}
478478
479479sub _mark_skipped {
480480 my ($self, $qid, $reason) = @_;
481481 my $dbh = $self->{db}->db_connect or return;
482482 my $r_q = $dbh->quote($reason || '');
483483 $self->{db}->db_readwrite($dbh, qq~
484484 UPDATE auto_ack_queue
485485 SET status='skipped', sent_at=NOW(), skip_reason=$r_q
486486 WHERE id=$qid LIMIT 1
487487 ~, __FILE__, __LINE__);
488488 $self->{db}->db_disconnect($dbh);
489489}
490490
491491sub _mark_failed {
492492 my ($self, $qid, $err) = @_;
493493 my $dbh = $self->{db}->db_connect or return;
494494 my $e_q = $dbh->quote($err || '');
495495 $self->{db}->db_readwrite($dbh, qq~
496496 UPDATE auto_ack_queue
497497 SET status='failed', sent_at=NOW(), error_message=$e_q
498498 WHERE id=$qid LIMIT 1
499499 ~, __FILE__, __LINE__);
500500 $self->{db}->db_disconnect($dbh);
501501}
502502
503503sub list_queued {
504504 my ($self, %a) = @_;
505505 my $limit = int($a{limit} || 30);
506506 my $dbh = $self->{db}->db_connect or return [];
507507 my @rows = $self->{db}->db_readwrite_multiple($dbh, qq~
508508 SELECT q.*, t.name AS template_name, r.name AS rule_name
509509 FROM auto_ack_queue q
510510 LEFT JOIN canned_templates t ON t.id = q.template_id
511511 LEFT JOIN auto_ack_rules r ON r.id = q.rule_id
512512 WHERE q.status='queued'
513513 ORDER BY q.fire_at ASC
514514 LIMIT $limit
515515 ~, __FILE__, __LINE__);
516516 $self->{db}->db_disconnect($dbh);
517517 return \@rows;
518518}
519519
520520sub list_history {
521521 my ($self, %a) = @_;
522522 my $limit = int($a{limit} || 50);
523523 my $dbh = $self->{db}->db_connect or return [];
524524 my @rows = $self->{db}->db_readwrite_multiple($dbh, qq~
525525 SELECT q.*, t.name AS template_name, r.name AS rule_name
526526 FROM auto_ack_queue q
527527 LEFT JOIN canned_templates t ON t.id = q.template_id
528528 LEFT JOIN auto_ack_rules r ON r.id = q.rule_id
529529 WHERE q.status IN ('sent','skipped','failed')
530530 ORDER BY COALESCE(q.sent_at, q.discovered_at) DESC
531531 LIMIT $limit
532532 ~, __FILE__, __LINE__);
533533 $self->{db}->db_disconnect($dbh);
534534 return \@rows;
535535}
536536
5375371;