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

O Operator
Diff

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

added on local at 2026-07-10 14:01:50

Added
+0
lines
Removed
-0
lines
Context
242
unchanged
Blobs
from acc10f1016a2
to acc10f1016a2
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::CannedReplies;
22#======================================================================
33# Meta-Admin -- canned support-reply templates.
44#
55# Operator picks a template from a dropdown on the /support.cgi thread
66# detail; body is server-side token-expanded against thread context and
77# dropped into the reply textarea (still editable before send).
88#
99# Templates live in Meta-Admin's own DB (canned_templates table). Auto-
1010# ack (scheduled fire) will read from the same table later.
1111#
1212# Supported merge tokens (see also the SQL migration file header):
1313# {customer_name}, {customer_first}, {customer_email},
1414# {subject}, {site_name}, {days_since_open},
1515# {other_open_thread_count}, {other_open_thread_subjects}
1616#======================================================================
1717use strict;
1818use warnings;
1919use MODS::DBConnect;
2020use MODS::MetaAdmin::SiteConn;
2121
2222sub new {
2323 my ($class) = @_;
2424 return bless({
2525 db => MODS::DBConnect->new,
2626 sc => MODS::MetaAdmin::SiteConn->new,
2727 err => '',
2828 }, $class);
2929}
3030
3131sub last_error { return $_[0]->{err} || ''; }
3232
3333#---------------------------------------------------------------------
3434# list({ active_only }) -> arrayref of { id, name, description, body,
3535# sort_order, is_active,
3636# created_at, updated_at }
3737#---------------------------------------------------------------------
3838sub list {
3939 my ($self, %a) = @_;
4040 my $active_only = exists $a{active_only} ? $a{active_only} : 0;
4141 my $where = $active_only ? 'WHERE is_active=1' : '';
4242 my $dbh = $self->{db}->db_connect;
4343 return [] unless $dbh;
4444 my @rows = $self->{db}->db_readwrite_multiple($dbh, qq~
4545 SELECT id, name, description, body, sort_order, is_active,
4646 created_at, updated_at
4747 FROM canned_templates
4848 $where
4949 ORDER BY sort_order, id
5050 ~, __FILE__, __LINE__);
5151 $self->{db}->db_disconnect($dbh);
5252 return \@rows;
5353}
5454
5555#---------------------------------------------------------------------
5656# get($id) -> single row hashref or undef
5757#---------------------------------------------------------------------
5858sub get {
5959 my ($self, $id) = @_;
6060 $id = int($id || 0);
6161 return undef unless $id > 0;
6262 my $dbh = $self->{db}->db_connect;
6363 return undef unless $dbh;
6464 my $r = $self->{db}->db_readwrite($dbh, qq~
6565 SELECT id, name, description, body, sort_order, is_active,
6666 created_at, updated_at
6767 FROM canned_templates
6868 WHERE id = $id
6969 LIMIT 1
7070 ~, __FILE__, __LINE__);
7171 $self->{db}->db_disconnect($dbh);
7272 return $r;
7373}
7474
7575#---------------------------------------------------------------------
7676# save({ id?, name, description, body, sort_order, is_active })
7777# id omitted -> insert; id set -> update.
7878# Returns the id on success, undef on failure.
7979#---------------------------------------------------------------------
8080sub save {
8181 my ($self, %a) = @_;
8282 my $id = int($a{id} || 0);
8383 my $name = defined $a{name} ? $a{name} : '';
8484 my $desc = defined $a{description} ? $a{description} : '';
8585 my $body = defined $a{body} ? $a{body} : '';
8686 my $sort_ord = int($a{sort_order} || 100);
8787 my $active = ($a{is_active} // 1) ? 1 : 0;
8888
8989 unless (length $name && length $body) {
9090 $self->{err} = 'name and body are required';
9191 return undef;
9292 }
9393
9494 my $dbh = $self->{db}->db_connect;
9595 return undef unless $dbh;
9696 my $name_q = $dbh->quote($name);
9797 my $desc_q = $dbh->quote($desc);
9898 my $body_q = $dbh->quote($body);
9999
100100 if ($id > 0) {
101101 $self->{db}->db_readwrite($dbh, qq~
102102 UPDATE canned_templates
103103 SET name = $name_q,
104104 description = $desc_q,
105105 body = $body_q,
106106 sort_order = $sort_ord,
107107 is_active = $active
108108 WHERE id = $id
109109 LIMIT 1
110110 ~, __FILE__, __LINE__);
111111 } else {
112112 $self->{db}->db_readwrite($dbh, qq~
113113 INSERT INTO canned_templates
114114 (name, description, body, sort_order, is_active, created_at)
115115 VALUES
116116 ($name_q, $desc_q, $body_q, $sort_ord, $active, NOW())
117117 ~, __FILE__, __LINE__);
118118 my $r = $self->{db}->db_readwrite($dbh,
119119 "SELECT LAST_INSERT_ID() AS id", __FILE__, __LINE__);
120120 $id = $r && $r->{id} ? $r->{id} : 0;
121121 }
122122 $self->{db}->db_disconnect($dbh);
123123 return $id;
124124}
125125
126126#---------------------------------------------------------------------
127127# toggle_active($id) -> flips is_active. Returns 1/0.
128128#---------------------------------------------------------------------
129129sub toggle_active {
130130 my ($self, $id) = @_;
131131 $id = int($id || 0);
132132 return 0 unless $id > 0;
133133 my $dbh = $self->{db}->db_connect;
134134 return 0 unless $dbh;
135135 $self->{db}->db_readwrite($dbh, qq~
136136 UPDATE canned_templates SET is_active = 1 - is_active
137137 WHERE id = $id LIMIT 1
138138 ~, __FILE__, __LINE__);
139139 $self->{db}->db_disconnect($dbh);
140140 return 1;
141141}
142142
143143#---------------------------------------------------------------------
144144# thread_context($thread_hashref) -> hashref of expansion values.
145145# Feed it the {thread => ..} half of MODS::MetaAdmin::SupportInbox::get_thread
146146# plus (optionally) the messages array. Handles the {other_open_thread_*}
147147# lookup by re-querying the site for the customer's other open threads.
148148#---------------------------------------------------------------------
149149sub thread_context {
150150 my ($self, $thread) = @_;
151151 my %ctx = (
152152 customer_name => '',
153153 customer_first => '',
154154 customer_email => '',
155155 subject => '',
156156 site_name => '',
157157 days_since_open => 0,
158158 other_open_thread_count => 0,
159159 other_open_thread_subjects => '',
160160 );
161161 return \%ctx unless $thread && ref $thread eq 'HASH';
162162
163163 $ctx{customer_email} = $thread->{customer_email} || '';
164164 my $name = $thread->{customer_name} || $thread->{customer_email} || '';
165165 # Strip anything after " (" (like "Shawn (Platform Owner)")
166166 $name =~ s/\s*\(.*//;
167167 $ctx{customer_name} = $name;
168168 my ($first) = split(/\s+/, $name);
169169 $ctx{customer_first} = $first || $name;
170170
171171 $ctx{subject} = $thread->{subject} || '';
172172 $ctx{site_name} = $thread->{display_name} || $thread->{slug} || '';
173173
174174 # Days since opened
175175 if ($thread->{created_at} && $thread->{created_at} =~ /^(\d{4})-(\d{2})-(\d{2})/) {
176176 require Time::Local;
177177 my $epoch = eval { Time::Local::timelocal(0, 0, 0, $3, $2 - 1, $1 - 1900) } || 0;
178178 $ctx{days_since_open} = $epoch ? int((time - $epoch) / 86400) : 0;
179179 }
180180
181181 # Other open threads by same customer on the same site.
182182 my $slug = $thread->{slug} || '';
183183 my $tid = int($thread->{id} || 0);
184184 my $cust = int($thread->{customer_user_id} || 0);
185185 if ($slug && $tid && $cust) {
186186 # Column names differ per site (opened_by_user_id on PT vs
187187 # customer_user_id elsewhere) -- ask SupportInbox for the adapter.
188188 require MODS::MetaAdmin::SupportInbox;
189189 # We don't want to instantiate SupportInbox just to peek at the
190190 # adapter table, so read the columns via a defensive query that
191191 # handles both column names. Simplest: try customer_user_id first,
192192 # fall back to opened_by_user_id.
193193 my $rows = $self->{sc}->query_many($slug, qq~
194194 SELECT id, subject
195195 FROM support_threads
196196 WHERE id != $tid
197197 AND status IN ('open','waiting_admin','waiting_customer','answered')
198198 AND (customer_user_id = $cust
199199 OR opened_by_user_id = $cust)
200200 ORDER BY last_message_at DESC
201201 LIMIT 5
202202 ~) || [];
203203 # Above uses OR against maybe-missing columns; sites that lack one
204204 # will error the whole query. Guard: on failure, try each column
205205 # separately.
206206 if (!@$rows && $self->{sc}->last_error) {
207207 $rows = $self->{sc}->query_many($slug, qq~
208208 SELECT id, subject FROM support_threads
209209 WHERE id != $tid AND customer_user_id = $cust
210210 AND status IN ('open','waiting_admin','waiting_customer')
211211 ORDER BY last_message_at DESC LIMIT 5
212212 ~) || [];
213213 }
214214 if (!@$rows) {
215215 $rows = $self->{sc}->query_many($slug, qq~
216216 SELECT id, subject FROM support_threads
217217 WHERE id != $tid AND opened_by_user_id = $cust
218218 AND status IN ('open','answered')
219219 ORDER BY last_message_at DESC LIMIT 5
220220 ~) || [];
221221 }
222222 $ctx{other_open_thread_count} = scalar @$rows;
223223 $ctx{other_open_thread_subjects} = join(', ',
224224 map { '"' . ($_->{subject} || '(no subject)') . '"' } @$rows);
225225 }
226226 return \%ctx;
227227}
228228
229229#---------------------------------------------------------------------
230230# expand($body, $ctx) -> string with {token} placeholders replaced.
231231# Unknown tokens left as-is so misspellings stay visible.
232232#---------------------------------------------------------------------
233233sub expand {
234234 my ($self, $body, $ctx) = @_;
235235 return '' unless defined $body;
236236 $ctx ||= {};
237237 my $out = $body;
238238 $out =~ s/\{([a-z_]+)\}/ exists $ctx->{$1} ? $ctx->{$1} : "{$1}" /ge;
239239 return $out;
240240}
241241
2422421;