Diff -- /var/www/vhosts/3dshawn.com/shop.3dshawn.com/admin_chat.cgi

O Operator
Diff

/var/www/vhosts/3dshawn.com/shop.3dshawn.com/admin_chat.cgi

added on local at 2026-07-11 18:36:17

Added
+552
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 8883b11d7cf9
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# ShopCart - Admin Chat
4#
5# Dedicated chat console: list of conversations on the left, thread on
6# the right, reply box at the bottom. Online/offline toggle controls
7# whether the chat widget appears on visitor-facing storefronts.
8#
9# Consolidates the former /admin_tracking_action.cgi (POST handler for
10# chat send, push link, online toggle, close/archive/reopen chat) via
11# SCRIPT_NAME dispatch. The _action.cgi file is a wrapper that `do`s
12# this file.
13#======================================================================
14use strict;
15use warnings;
16
17use lib '/var/www/vhosts/3dshawn.com/shop.3dshawn.com';
18use CGI;
19use MODS::Template;
20use MODS::DBConnect;
21use MODS::Login;
22use MODS::ShopCart::Config;
23use MODS::ShopCart::Wrapper;
24use MODS::ShopCart::Admin;
25use MODS::ShopCart::Tracking;
26
27my $q = CGI->new;
28my $form = $q->Vars;
29my $auth = MODS::Login->new;
30my $wrap = MODS::ShopCart::Wrapper->new;
31my $tfile = MODS::Template->new;
32my $db = MODS::DBConnect->new;
33my $cfg = MODS::ShopCart::Config->new;
34my $admin = MODS::ShopCart::Admin->new;
35my $tr = MODS::ShopCart::Tracking->new;
36my $DB = $cfg->settings('database_name');
37
38$|=1;
39my $userinfo = $auth->login_verify();
40if (!$userinfo) {
41 print "Status: 302 Found\nLocation: /login.cgi\n\n";
42 exit;
43}
44
45my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'admin_chat';
46
47if ($entry eq 'admin_tracking_action') {
48 # Action requires admin + 'admin_view_traffic' feature (stricter than
49 # the primary's 'admin_chat' feature -- inline here so we do not
50 # accidentally grant traffic-mutation actions to chat-only admins).
51 $admin->require_admin($userinfo);
52 require MODS::ShopCart::Permissions;
53 MODS::ShopCart::Permissions->new->require_feature($userinfo, 'admin_view_traffic');
54 _handle_action($q, $form);
55 exit;
56}
57
58$admin->require_admin($userinfo);
59require MODS::ShopCart::Permissions;
60MODS::ShopCart::Permissions->new->require_feature($userinfo, 'admin_chat');
61
62my $tutorial_mode = ($form->{tutorial} && $form->{tutorial} eq '1') ? 1 : 0;
63
64my $dbh = $db->db_connect();
65my $schema_ready = $tr->schema_ready($db, $dbh, $DB);
66
67my $admin_online = $schema_ready ? $tr->admin_online($db, $dbh, $DB) : 0;
68
69# View filter + search
70my %ok_view = (active=>1, closed=>1, archived=>1, all=>1);
71my $view = $form->{view} || 'active';
72$view = 'active' unless $ok_view{$view};
73my $search = defined $form->{q} ? $form->{q} : '';
74$search =~ s/^\s+|\s+$//g;
75my $search_trim = substr($search, 0, 100);
76
77# Conversation list
78my @chats_raw = $schema_ready ? $tr->admin_chat_list($db, $dbh, $DB,
79 filter => $view, search => $search_trim, limit => 100) : ();
80my $counts = $schema_ready ? $tr->chat_counts_by_status($db, $dbh, $DB)
81 : { active=>0, closed=>0, archived=>0, all=>0 };
82
83sub _h { my $s = shift; $s //= ''; $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g; $s =~ s/"/&quot;/g; return $s; }
84sub _qs { my $s = shift; $s //= ''; $s =~ s/([^A-Za-z0-9\-_.~])/sprintf('%%%02X', ord($1))/eg; return $s; }
85sub _achat_ts_span {
86 my ($s, $fallback) = @_;
87 $fallback = '' unless defined $fallback;
88 return $fallback unless $s && length $s;
89 return $s unless $s =~ /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/;
90 require Time::Local;
91 my $ep = eval { Time::Local::timelocal($6,$5,$4,$3,$2-1,$1-1900) } || 0;
92 return $s unless $ep;
93 return qq~<span class="ts" data-ts="$ep" data-fmt="datetime">$s</span>~;
94}
95
96# Build the ?view=X&q=... fragment so list links keep filter state.
97my $view_qs = '&view=' . $view;
98$view_qs .= '&q=' . _qs($search_trim) if length $search_trim;
99
100my @chats;
101my $selected_chat_id = $form->{chat} || 0;
102$selected_chat_id =~ s/[^0-9]//g;
103
104foreach my $c (@chats_raw) {
105 my $is_selected = ($selected_chat_id && $c->{chat_id} eq $selected_chat_id) ? 1 : 0;
106 $selected_chat_id ||= $c->{chat_id} if !$selected_chat_id;
107
108 # Pull session fingerprint for classification.
109 my $full = $db->db_readwrite($dbh, qq~
110 SELECT id, buyer_account_id FROM `${DB}`.tracking_sessions WHERE id='$c->{session_id}' LIMIT 1
111 ~, $ENV{SCRIPT_NAME}, __LINE__);
112 my $cls = $tr->classify_session($db, $dbh, $DB, $full);
113
114 my $chat_status = $c->{status} || 'open';
115 my $idle = $c->{idle_seconds} || 0;
116 push @chats, {
117 chat_id => $c->{chat_id},
118 session_id => $c->{session_id},
119 label => _h($c->{visitor_label} || ('Visitor #' . $c->{session_id})),
120 platform_meta => _h(($c->{os_name} || '') . ' / ' . ($c->{browser_name} || '')),
121 device => _h($c->{device_type} || ''),
122 current_page => _h($c->{current_page_path} || ''),
123 last_message_at => $c->{last_message_at} || '',
124 last_body => _h(substr($c->{last_body} || '', 0, 60)),
125 last_from => $c->{last_from} || '',
126 unread => $c->{unread_for_admin} || 0,
127 href => '/admin_chat.cgi?chat=' . $c->{chat_id} . $view_qs,
128 is_selected => $is_selected,
129 # Buyer classification (guest/lead/customer)
130 status => $cls->{status},
131 status_label => $cls->{status_label},
132 is_customer => $cls->{status} eq 'customer' ? 1 : 0,
133 is_lead => $cls->{status} eq 'lead' ? 1 : 0,
134 is_guest => $cls->{status} eq 'guest' ? 1 : 0,
135 email => _h($cls->{email} || ''),
136 has_email => $cls->{email} ? 1 : 0,
137 total_orders => $cls->{total_orders},
138 ltv_display => '$' . sprintf('%.2f', ($cls->{ltv_cents} || 0) / 100),
139 # Conversation status (open/closed/archived) for visual treatment
140 chat_status => $chat_status,
141 is_open => $chat_status eq 'open' ? 1 : 0,
142 is_closed => $chat_status eq 'closed' ? 1 : 0,
143 is_archived => $chat_status eq 'archived' ? 1 : 0,
144 visitor_online => ($c->{is_online} && $idle < 60) ? 1 : 0,
145 awaiting_reply => (($c->{last_from} || '') eq 'visitor' && $c->{unread_for_admin}) ? 1 : 0,
146 };
147}
148
149# Selected thread + visitor details
150my @thread_rows;
151my $thread = {};
152if ($schema_ready && $selected_chat_id) {
153 # Mark read on view
154 $tr->mark_chat_read($db, $dbh, $DB, $selected_chat_id, 'admin');
155
156 my @msgs = $tr->chat_messages($db, $dbh, $DB, $selected_chat_id, 200);
157 foreach my $m (@msgs) {
158 # HTML-escape first, then linkify URLs. Order matters: escaping
159 # an already-built <a> tag would mangle it; URLs themselves
160 # don't contain HTML-special chars normally, and any that do
161 # (& in querystrings) are correctly &amp;-escaped inside the
162 # href attribute, which is what HTML attribute parsing expects.
163 my $safe = _h($m->{body});
164 $safe =~ s{(https?://[^\s<]+)}{<a href="$1" target="_blank" rel="noopener" style="color:#60a5fa;text-decoration:underline">$1</a>}g;
165 push @thread_rows, {
166 id => $m->{id},
167 from_role => $m->{from_role},
168 body => $safe,
169 sent_at => _achat_ts_span($m->{sent_at}, ''),
170 is_admin => ($m->{from_role} eq 'admin') ? 1 : 0,
171 is_visitor => ($m->{from_role} eq 'visitor') ? 1 : 0,
172 is_system => ($m->{from_role} eq 'system') ? 1 : 0,
173 };
174 }
175
176 # Visitor info for the right rail + chat row for status-driven actions
177 my $sess = $db->db_readwrite($dbh, qq~
178 SELECT s.*, c.status AS chat_status FROM `${DB}`.support_chats c
179 JOIN `${DB}`.tracking_sessions s ON s.id = c.session_id
180 WHERE c.id='$selected_chat_id' LIMIT 1
181 ~, $ENV{SCRIPT_NAME}, __LINE__);
182 if ($sess && $sess->{id}) {
183 my $cls = $tr->classify_session($db, $dbh, $DB, $sess);
184 my $chat_status = $sess->{chat_status} || 'open';
185 $thread = {
186 session_id => $sess->{id},
187 label => _h($sess->{visitor_label} || ''),
188 os => _h(($sess->{os_name} || '') . ' ' . ($sess->{os_version} || '')),
189 browser => _h(($sess->{browser_name} || '') . ' ' . ($sess->{browser_version} || '')),
190 device => _h($sess->{device_type} || ''),
191 screen => ($sess->{screen_width} && $sess->{screen_height}) ? ($sess->{screen_width} . 'x' . $sess->{screen_height}) : '',
192 language => _h($sess->{language} || ''),
193 timezone => _h($sess->{timezone} || ''),
194 current_page => _h($sess->{current_page_path} || ''),
195 status => $cls->{status},
196 status_label => $cls->{status_label},
197 is_customer => $cls->{status} eq 'customer' ? 1 : 0,
198 is_lead => $cls->{status} eq 'lead' ? 1 : 0,
199 is_guest => $cls->{status} eq 'guest' ? 1 : 0,
200 email => _h($cls->{email} || ''),
201 has_email => $cls->{email} ? 1 : 0,
202 total_orders => $cls->{total_orders},
203 ltv_display => '$' . sprintf('%.2f', ($cls->{ltv_cents} || 0) / 100),
204 # Conversation status drives which header buttons render.
205 chat_status => $chat_status,
206 is_open => $chat_status eq 'open' ? 1 : 0,
207 is_closed => $chat_status eq 'closed' ? 1 : 0,
208 is_archived => $chat_status eq 'archived' ? 1 : 0,
209 };
210 }
211}
212
213# ---- Concluded-chats summary (last 30 days + today) -----------------
214# Counts every chat that hit status='closed' OR 'archived' (both count
215# as "resolved" -- archive is just an old close). Today's bar always
216# renders even when zero closures so the trend reads honestly. Also
217# pulls the individual chats so the bar drilldown overlay can list
218# them without a follow-up request.
219my @resolved_raw = $schema_ready ? $tr->chats_closed_by_day($db, $dbh, $DB, 30) : ();
220my @resolved_list = $schema_ready ? $tr->chats_closed_list($db, $dbh, $DB, 30) : ();
221my %resolved_by_d = map { $_->{d} => $_ } @resolved_raw;
222my %chats_by_d;
223foreach my $cc (@resolved_list) {
224 my $d = $cc->{d} || '';
225 push @{ $chats_by_d{$d} }, {
226 id => $cc->{id} + 0,
227 hm => $cc->{hm} || '',
228 label => $cc->{label} || ('Visitor #' . $cc->{id}),
229 };
230}
231
232# Build the trailing 30 days from oldest -> newest so the chart reads
233# left-to-right as time-moves-forward.
234my @resolved_days;
235my $resolved_max = 0;
236my $today_count = 0;
237my @short_months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
238# Today's date as a reference point. We walk back 29 days and stop at today.
239my ($t_sec,$t_min,$t_hour,$t_mday,$t_mon,$t_year) = localtime(time);
240require POSIX;
241my $today_epoch = POSIX::mktime(0,0,12,$t_mday,$t_mon,$t_year);
242for (my $i = 29; $i >= 0; $i--) {
243 my $epoch = $today_epoch - ($i * 86400);
244 my @t = localtime($epoch);
245 my $d_iso = sprintf('%04d-%02d-%02d', $t[5]+1900, $t[4]+1, $t[3]);
246 my $d_short = sprintf('%s %d', $short_months[$t[4]], $t[3]); # "May 17"
247 my $n = ($resolved_by_d{$d_iso} && $resolved_by_d{$d_iso}->{n}) || 0;
248 $resolved_max = $n if $n > $resolved_max;
249 $today_count = $n if $i == 0;
250 # Serialise the per-day chat list so the drilldown can read it
251 # from a data-* attribute. Embeds visitor label + close time +
252 # link to the conversation. Strict JSON (double-quoted keys/values
253 # with JSON-escaping) so the JS can JSON.parse it cleanly.
254 my $chats_for_day = $chats_by_d{$d_iso} || [];
255 my @items_json;
256 for my $cc (@$chats_for_day) {
257 my $lbl = $cc->{label};
258 # JSON-escape: backslash, double-quote, control chars
259 $lbl =~ s/\\/\\\\/g;
260 $lbl =~ s/"/\\"/g;
261 $lbl =~ s/\r/\\r/g;
262 $lbl =~ s/\n/\\n/g;
263 $lbl =~ s/\t/\\t/g;
264 my $hm = $cc->{hm} || '';
265 $hm =~ s/[^0-9:]//g;
266 my $cid = $cc->{id} + 0;
267 # Build with sprintf instead of qq~ so a literal '~' in the
268 # visitor label cannot accidentally terminate the heredoc.
269 push @items_json, sprintf('{"id":%d,"hm":"%s","label":"%s"}', $cid, $hm, $lbl);
270 }
271 my $chats_json = '[' . join(',', @items_json) . ']';
272 # HTML-attribute escape the JSON so it's safe inside data-chats='...'.
273 # Single-quote attribute means we only need to escape the apostrophe.
274 $chats_json =~ s/&/&amp;/g;
275 $chats_json =~ s/'/&#39;/g;
276 push @resolved_days, {
277 d_iso => $d_iso,
278 d_short => $d_short,
279 d_label => $d_short,
280 n => $n,
281 is_current => $i == 0 ? 1 : 0,
282 chats_json => $chats_json,
283 };
284}
285# Add a relative-height percent for the bar visual.
286foreach my $r (@resolved_days) {
287 $r->{bar_pct} = $resolved_max ? sprintf('%.1f', ($r->{n} / $resolved_max) * 100) : '0.0';
288}
289my $resolved_total_30d = 0;
290$resolved_total_30d += $_->{n} for @resolved_days;
291# Preserve the old var names so unrelated template branches keep working.
292my $mtd_resolved = $today_count;
293my @resolved_months = @resolved_days;
294my $resolved_total_6mo = $resolved_total_30d;
295
296$db->db_disconnect($dbh);
297
298my $tvars = {
299 user_id => $userinfo->{user_id},
300 schema_ready => $schema_ready ? 1 : 0,
301 schema_missing => $schema_ready ? 0 : 1,
302 admin_online => $admin_online,
303
304 # Monthly resolved-chats summary
305 resolved_months => \@resolved_months,
306 has_resolved_months => scalar(@resolved_months) ? 1 : 0,
307 resolved_total_6mo => $resolved_total_6mo,
308 resolved_mtd => $mtd_resolved,
309 chats => \@chats,
310 has_chats => scalar(@chats) ? 1 : 0,
311 chat_count => scalar(@chats),
312 selected_chat_id => $selected_chat_id || 0,
313 has_selected => $selected_chat_id ? 1 : 0,
314 thread => $thread,
315 has_thread => %$thread ? 1 : 0,
316 thread_rows => \@thread_rows,
317 has_thread_rows => scalar(@thread_rows) ? 1 : 0,
318 tutorial_mode => $tutorial_mode,
319 not_tutorial_mode=> $tutorial_mode ? 0 : 1,
320 # Filter + search state
321 view => $view,
322 is_view_active => $view eq 'active' ? 1 : 0,
323 is_view_closed => $view eq 'closed' ? 1 : 0,
324 is_view_archived => $view eq 'archived' ? 1 : 0,
325 is_view_all => $view eq 'all' ? 1 : 0,
326 search_q => _h($search_trim),
327 has_search => length $search_trim ? 1 : 0,
328 count_active => $counts->{active},
329 count_closed => $counts->{closed},
330 count_archived => $counts->{archived},
331 count_all => $counts->{all},
332};
333
334# Tutorial sample
335if ($tutorial_mode) {
336 $tvars->{admin_online} = 1;
337 $tvars->{count_active} = 3;
338 $tvars->{count_closed} = 12;
339 $tvars->{count_archived} = 48;
340 $tvars->{count_all} = 63;
341 # Sample daily resolved-chats trend (30 trailing days). Numbers
342 # picked to look organic (busier weekdays, quieter weekends) without
343 # spiking off-chart so the bar visual reads naturally in tutorial.
344 my @sample_pattern = (4,6,5,7,3,2,1, 5,8,9,7,6,2,3, 6,7,10,8,9,4,2, 5,9,11,8,7,3,1, 6,12);
345 my @sample_days;
346 my @short_months_tut = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
347 my ($s_sec,$s_min,$s_hour,$s_mday,$s_mon,$s_year) = localtime(time);
348 require POSIX;
349 my $s_today_epoch = POSIX::mktime(0,0,12,$s_mday,$s_mon,$s_year);
350 my $sample_total = 0;
351 my $sample_today = 0;
352 my $sample_max = 0;
353 for (my $i = 29; $i >= 0; $i--) {
354 my $n = $sample_pattern[29 - $i] || 0;
355 my @t = localtime($s_today_epoch - ($i * 86400));
356 my $d_iso = sprintf('%04d-%02d-%02d', $t[5]+1900, $t[4]+1, $t[3]);
357 my $d_short = sprintf('%s %d', $short_months_tut[$t[4]], $t[3]);
358 $sample_total += $n;
359 $sample_today = $n if $i == 0;
360 $sample_max = $n if $n > $sample_max;
361 push @sample_days, {
362 d_iso => $d_iso,
363 d_short => $d_short,
364 d_label => $d_short,
365 n => $n,
366 is_current => $i == 0 ? 1 : 0,
367 chats_json => '[]',
368 };
369 }
370 foreach my $m (@sample_days) {
371 $m->{bar_pct} = $sample_max ? sprintf('%.1f', ($m->{n} / $sample_max) * 100) : '0.0';
372 }
373 $tvars->{resolved_months} = \@sample_days;
374 $tvars->{has_resolved_months} = 1;
375 $tvars->{resolved_total_6mo} = $sample_total;
376 $tvars->{resolved_mtd} = $sample_today;
377 $tvars->{chats} = [
378 { chat_id=>1, session_id=>842, label=>'Visitor #842', platform_meta=>'macOS / Safari', device=>'desktop',
379 current_page=>'/store.cgi?id=1', last_message_at=>'2 min ago',
380 last_body=>'Do these prints come with supports?', last_from=>'visitor',
381 unread=>2, href=>'/admin_chat.cgi?tutorial=1&chat=1', is_selected=>1,
382 status=>'customer', status_label=>'Customer', is_customer=>1, is_lead=>0, is_guest=>0,
383 email=>'rune.caster@example.com', has_email=>1, total_orders=>4, ltv_display=>'$96.00',
384 chat_status=>'open', is_open=>1, is_closed=>0, is_archived=>0, visitor_online=>1, awaiting_reply=>1 },
385 { chat_id=>2, session_id=>841, label=>'Visitor #841', platform_meta=>'Windows / Chrome', device=>'desktop',
386 current_page=>'/store.cgi?id=1&page=catalog', last_message_at=>'18 min ago',
387 last_body=>'Thanks, that worked!', last_from=>'visitor',
388 unread=>0, href=>'/admin_chat.cgi?tutorial=1&chat=2', is_selected=>0,
389 status=>'guest', status_label=>'Guest', is_customer=>0, is_lead=>0, is_guest=>1,
390 email=>'', has_email=>0, total_orders=>0, ltv_display=>'$0.00',
391 chat_status=>'open', is_open=>1, is_closed=>0, is_archived=>0, visitor_online=>0, awaiting_reply=>0 },
392 { chat_id=>3, session_id=>840, label=>'Visitor #840', platform_meta=>'iOS / Safari', device=>'mobile',
393 current_page=>'/store.cgi?id=1&page=about', last_message_at=>'1 h ago',
394 last_body=>'Glad you could help!', last_from=>'admin',
395 unread=>0, href=>'/admin_chat.cgi?tutorial=1&chat=3', is_selected=>0,
396 status=>'lead', status_label=>'Lead', is_customer=>0, is_lead=>1, is_guest=>0,
397 email=>'bench.hammer@example.com', has_email=>1, total_orders=>0, ltv_display=>'$0.00',
398 chat_status=>'open', is_open=>1, is_closed=>0, is_archived=>0, visitor_online=>0, awaiting_reply=>0 },
399 ];
400 $tvars->{has_chats} = 1;
401 $tvars->{chat_count} = 3;
402 $tvars->{selected_chat_id} = 1;
403 $tvars->{has_selected} = 1;
404 $tvars->{thread} = {
405 session_id=>842, label=>'Visitor #842',
406 os=>'macOS 14.4', browser=>'Safari 17.4', device=>'desktop',
407 screen=>'2560x1440', language=>'en-US', timezone=>'America/Denver',
408 current_page=>'/store.cgi?id=1',
409 status=>'customer', status_label=>'Customer', is_customer=>1, is_lead=>0, is_guest=>0,
410 email=>'rune.caster@example.com', has_email=>1, total_orders=>4, ltv_display=>'$96.00',
411 chat_status=>'open', is_open=>1, is_closed=>0, is_archived=>0,
412 };
413 $tvars->{has_thread} = 1;
414 $tvars->{thread_rows} = [
415 { id=>1, from_role=>'visitor', body=>'Hi! Quick question about the Dragon Lord MK-II.', sent_at=>'14:18', is_admin=>0, is_visitor=>1, is_system=>0 },
416 { id=>2, from_role=>'admin', body=>'Hey! Happy to help -- what is the question?', sent_at=>'14:19', is_admin=>1, is_visitor=>0, is_system=>0 },
417 { id=>3, from_role=>'visitor', body=>'Do these prints come with supports?', sent_at=>'14:20', is_admin=>0, is_visitor=>1, is_system=>0 },
418 { id=>4, from_role=>'visitor', body=>'And what layer height did you test at?', sent_at=>'14:20', is_admin=>0, is_visitor=>1, is_system=>0 },
419 ];
420 $tvars->{has_thread_rows} = 1;
421 $tvars->{schema_ready} = 1;
422 $tvars->{schema_missing} = 0;
423}
424
425print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
426
427my $body = join('', $tfile->template('shopcart_admin_chat.html', $tvars, $userinfo));
428
429$wrap->render({
430 userinfo => $userinfo,
431 page_key => 'admin_chat',
432 title => 'Admin &middot; Chat',
433 body => $body,
434});
435
436#======================================================================
437# admin_tracking_action entry: POST handler
438# act=send_message -- admin replies in a chat
439# act=push_link -- push a URL to a visitor session
440# act=toggle_online -- flip admin online/offline (widget visibility)
441# act=close_chat -- close an active chat
442# act=archive_chat -- archive a chat
443# act=reopen_chat -- reopen a closed/archived chat
444#======================================================================
445sub _handle_action {
446 my ($q, $form) = @_;
447
448 my $admin_id = $userinfo->{user_id};
449 $admin_id =~ s/[^0-9]//g;
450
451 my $act = $form->{act} || '';
452 my $back_to = $form->{back_to} || '';
453
454 my $dbh = $db->db_connect();
455 unless ($tr->schema_ready($db, $dbh, $DB)) {
456 $db->db_disconnect($dbh);
457 _back($back_to, '?err=schema');
458 return;
459 }
460
461 if ($act eq 'send_message') {
462 my $chat_id = $form->{chat_id}; $chat_id =~ s/[^0-9]//g;
463 my $body = $form->{body} || '';
464 if ($chat_id && length $body) {
465 $tr->post_chat_message($db, $dbh, $DB,
466 chat_id => $chat_id,
467 from_role => 'admin',
468 admin_user_id => $admin_id,
469 body => $body,
470 );
471 }
472 $db->db_disconnect($dbh);
473 _back($back_to, '?chat=' . ($chat_id || ''));
474 return;
475 }
476
477 if ($act eq 'push_link') {
478 my $sid = $form->{session_id}; $sid =~ s/[^0-9]//g;
479 my $url = $form->{url} || '';
480 my $label = $form->{label} || '';
481 if ($sid && length $url) {
482 $tr->push_link($db, $dbh, $DB,
483 session_id => $sid,
484 admin_user_id => $admin_id,
485 url => $url,
486 label => $label,
487 open_mode => $form->{open_mode} || 'new_tab',
488 );
489 }
490 $db->db_disconnect($dbh);
491 if ($back_to eq 'chat') {
492 my $chat_id = $form->{chat_id} || '';
493 $chat_id =~ s/[^0-9]//g;
494 _back('chat', '?chat=' . $chat_id . '&ok=pushed');
495 } else {
496 _back('visitors', '?id=' . $sid . '&ok=pushed');
497 }
498 return;
499 }
500
501 if ($act eq 'toggle_online') {
502 my $value = $form->{value};
503 $value = ($value && $value eq '1') ? 1 : 0;
504 $tr->set_admin_online($db, $dbh, $DB, $value, $admin_id);
505 $db->db_disconnect($dbh);
506 _back('chat', '');
507 return;
508 }
509
510 if ($act eq 'close_chat') {
511 my $chat_id = $form->{chat_id}; $chat_id =~ s/[^0-9]//g;
512 if ($chat_id) {
513 $tr->close_chat_by_admin($db, $dbh, $DB, $chat_id, $admin_id);
514 }
515 $db->db_disconnect($dbh);
516 # Land on the Closed view with this chat selected -- otherwise it
517 # vanishes from the default Active list and looks broken.
518 _back('chat', '?view=closed&chat=' . ($chat_id || ''));
519 return;
520 }
521
522 if ($act eq 'archive_chat') {
523 my $chat_id = $form->{chat_id}; $chat_id =~ s/[^0-9]//g;
524 if ($chat_id) {
525 $tr->archive_chat_by_admin($db, $dbh, $DB, $chat_id, $admin_id);
526 }
527 $db->db_disconnect($dbh);
528 _back('chat', '?view=archived&chat=' . ($chat_id || ''));
529 return;
530 }
531
532 if ($act eq 'reopen_chat') {
533 my $chat_id = $form->{chat_id}; $chat_id =~ s/[^0-9]//g;
534 if ($chat_id) {
535 $tr->reopen_chat_by_admin($db, $dbh, $DB, $chat_id, $admin_id);
536 }
537 $db->db_disconnect($dbh);
538 _back('chat', '?view=active&chat=' . ($chat_id || ''));
539 return;
540 }
541
542 $db->db_disconnect($dbh);
543 _back($back_to, '?err=unknown_act');
544}
545
546sub _back {
547 my ($where, $qs) = @_;
548 my $loc = ($where eq 'visitors') ? '/admin_visitors.cgi'
549 : ($where eq 'chat') ? '/admin_chat.cgi'
550 : '/admin_chat.cgi';
551 print "Status: 302 Found\nLocation: $loc$qs\n\n";
552}