added on local at 2026-07-01 12:34:06
| 1 | #!/usr/bin/perl | |
| 2 | ||
| 3 | ################################################################################################################################## | |
| 4 | # ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____ | |
| 5 | # / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \ | |
| 6 | # / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / / | |
| 7 | # / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ / | |
| 8 | # \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/ | |
| 9 | # | |
| 10 | # - Written by: Shawn Pickering | |
| 11 | # - shawn@shawnpickering.com | |
| 12 | ################################################################################################################################## | |
| 13 | use strict; | |
| 14 | use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com'; | |
| 15 | ||
| 16 | ||
| 17 | ############################################################# | |
| 18 | ## Variables | |
| 19 | ############################################################# | |
| 20 | my $TRIGGERS = [ | |
| 21 | ['task_created', 'When a task is created'], | |
| 22 | ['task_status_changed', 'When a task changes status'], | |
| 23 | ['task_assigned', 'When a task is reassigned'], | |
| 24 | ['custom_field_set', 'When a custom field is set / changed'], | |
| 25 | ['department_routed', 'When a ticket is routed to a department'], | |
| 26 | ]; | |
| 27 | my $STATUSES = [qw(open in_progress blocked review done cancelled)]; | |
| 28 | my $PRIORITIES = [qw(low medium high urgent critical)]; | |
| 29 | my $ACTIONS = [ | |
| 30 | ['set_priority', 'Set priority to...'], | |
| 31 | ['set_status', 'Set status to...'], | |
| 32 | ['assign_user', 'Assign to user...'], | |
| 33 | ['add_comment', 'Post a comment...'], | |
| 34 | ['notify_user', 'Send a notification to...'], | |
| 35 | ['send_webhook', 'Send outbound webhook (any HTTPS endpoint)...'], | |
| 36 | ]; | |
| 37 | ||
| 38 | ||
| 39 | ||
| 40 | ############################################################ | |
| 41 | # Modules | |
| 42 | ############################################################ | |
| 43 | use CGI; my $query = new CGI; my $form = $query->Vars; | |
| 44 | use MODS::Login; my $login = new MODS::Login; | |
| 45 | use MODS::PTMatrix::Urls; my $getlist = new MODS::PTMatrix::Urls; my $url = $getlist->urls(); | |
| 46 | use MODS::Template; my $tfile = new MODS::Template; | |
| 47 | use MODS::PTMatrix::Wrapper; my $load = new MODS::PTMatrix::Wrapper; | |
| 48 | use MODS::DBConnect; my $db = new MODS::DBConnect; | |
| 49 | use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config; my $database_name = $config->settings('database_name'); | |
| 50 | use MODS::PTMatrix::PM; | |
| 51 | ||
| 52 | ||
| 53 | ||
| 54 | ############################################################# | |
| 55 | ## Get form data | |
| 56 | ############################################################# | |
| 57 | my %rawform = %$form; | |
| 58 | ||
| 59 | foreach my $line(keys %$form){ | |
| 60 | $form->{$line} =~ s/[^!-~\s]//g; | |
| 61 | $form->{$line} = quotemeta("$form->{$line}"); | |
| 62 | } | |
| 63 | ||
| 64 | ||
| 65 | ||
| 66 | ############################################################ | |
| 67 | # Program Controls | |
| 68 | ############################################################ | |
| 69 | $|=1; | |
| 70 | my $userinfo = $login->login_verify(); | |
| 71 | print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~; | |
| 72 | if(!$userinfo){print qq~<script>window.location="$url->{login}";</script>~;} | |
| 73 | elsif(!MODS::PTMatrix::PM::get_company_id($userinfo)){print qq~<script>window.location="$url->{admin_companies}";</script>~;} | |
| 74 | elsif(!MODS::PTMatrix::PM::is_company_admin($userinfo)){print qq~<script>window.location="$url->{dashboard}";</script>~;} | |
| 75 | elsif(($ENV{REQUEST_METHOD}||'') eq 'POST'){&automation_action;} | |
| 76 | else{&automations_list;} | |
| 77 | ||
| 78 | ||
| 79 | ############################################################ | |
| 80 | # Subroutines | |
| 81 | ############################################################ | |
| 82 | #Build a <select> option list from a flat list or list-of-pairs | |
| 83 | sub opts{ | |
| 84 | my($list, $selected, $blank) = @_; | |
| 85 | my $h = $blank ? qq~<option value="">$blank</option>~ : ''; | |
| 86 | for my $row(@$list){ | |
| 87 | my($val, $label) = ref($row) ? @$row : ($row, $row); | |
| 88 | my $sel = (defined($selected) && $selected eq $val) ? ' selected' : ''; | |
| 89 | $h .= qq~<option value="$val"$sel>$label</option>~; | |
| 90 | } | |
| 91 | return $h; | |
| 92 | } | |
| 93 | ||
| 94 | sub proj_opts{ | |
| 95 | my($projects, $selected) = @_; | |
| 96 | my $h = '<option value="">— any project —</option>'; | |
| 97 | for my $p(@$projects){ | |
| 98 | my $sel = ($selected && $p->{id} == $selected) ? ' selected' : ''; | |
| 99 | my $disp = MODS::PTMatrix::PM::h("$p->{key_prefix} — $p->{name}"); | |
| 100 | $h .= qq~<option value="$p->{id}"$sel>$disp</option>~; | |
| 101 | } | |
| 102 | return $h; | |
| 103 | } | |
| 104 | ||
| 105 | sub user_opts{ | |
| 106 | my($users, $selected) = @_; | |
| 107 | my $h = '<option value="">— pick a user —</option>'; | |
| 108 | for my $usr(@$users){ | |
| 109 | my $sel = ($selected && $usr->{id} == $selected) ? ' selected' : ''; | |
| 110 | my $disp = MODS::PTMatrix::PM::h($usr->{display_name}); | |
| 111 | $h .= qq~<option value="$usr->{id}"$sel>$disp</option>~; | |
| 112 | } | |
| 113 | return $h; | |
| 114 | } | |
| 115 | ||
| 116 | sub automation_action{ | |
| 117 | my $cid = MODS::PTMatrix::PM::get_company_id($userinfo); | |
| 118 | my $uid = MODS::PTMatrix::PM::get_user_id($userinfo); | |
| 119 | ||
| 120 | my $dbh = $db->db_connect(); | |
| 121 | my $act = $form->{act} || ''; | |
| 122 | my $flash = ''; | |
| 123 | ||
| 124 | if($act eq 'save'){ | |
| 125 | my $id = MODS::PTMatrix::PM::safe_int($form->{id}); | |
| 126 | my $name = $rawform{name} || ''; | |
| 127 | my $pid = MODS::PTMatrix::PM::safe_int($form->{project_id}); | |
| 128 | my $is_active = $form->{is_active} ? 1 : 0; | |
| 129 | ||
| 130 | #Allowlist every selectable - drop anything we don't recognize | |
| 131 | my $trigger = $form->{trigger_kind} || ''; | |
| 132 | $trigger = '' unless grep { $_->[0] eq $trigger } @$TRIGGERS; | |
| 133 | my $from_s = $form->{trigger_from_status} || ''; | |
| 134 | $from_s = '' unless grep { $_ eq $from_s } @$STATUSES; | |
| 135 | my $to_s = $form->{trigger_to_status} || ''; | |
| 136 | $to_s = '' unless grep { $_ eq $to_s } @$STATUSES; | |
| 137 | my $cond_p = $form->{cond_priority} || ''; | |
| 138 | $cond_p = '' unless grep { $_ eq $cond_p } @$PRIORITIES; | |
| 139 | my $action = $form->{action_kind} || ''; | |
| 140 | $action = '' unless grep { $_->[0] eq $action } @$ACTIONS; | |
| 141 | my $a_prio = $form->{action_priority} || ''; | |
| 142 | $a_prio = '' unless grep { $_ eq $a_prio } @$PRIORITIES; | |
| 143 | my $a_status = $form->{action_status} || ''; | |
| 144 | $a_status = '' unless grep { $_ eq $a_status } @$STATUSES; | |
| 145 | ||
| 146 | my $a_uid = MODS::PTMatrix::PM::safe_int($form->{action_user_id}); | |
| 147 | my $a_text = substr($rawform{action_comment_text} || '', 0, 500); | |
| 148 | my $a_webhook = $rawform{action_webhook_url} || ''; | |
| 149 | $a_webhook = '' unless $a_webhook =~ m{^https?://}; | |
| 150 | $a_webhook = substr($a_webhook, 0, 500); | |
| 151 | ||
| 152 | if(!$name || !$trigger || !$action){ | |
| 153 | $flash = 'Name, trigger, and action are required.'; | |
| 154 | }else{ | |
| 155 | my $name_q = MODS::PTMatrix::PM::sql_quote(substr($name, 0, 120)); | |
| 156 | my $text_q = MODS::PTMatrix::PM::sql_quote($a_text); | |
| 157 | my $webhook_q = MODS::PTMatrix::PM::sql_quote($a_webhook); | |
| 158 | my $pid_sql = $pid ? "'$pid'" : 'NULL'; | |
| 159 | my $from_sql = length($from_s) ? "'$from_s'" : 'NULL'; | |
| 160 | my $to_sql = length($to_s) ? "'$to_s'" : 'NULL'; | |
| 161 | my $cp_sql = length($cond_p) ? "'$cond_p'" : 'NULL'; | |
| 162 | my $ap_sql = length($a_prio) ? "'$a_prio'" : 'NULL'; | |
| 163 | my $as_sql = length($a_status)? "'$a_status'" : 'NULL'; | |
| 164 | my $au_sql = $a_uid ? "'$a_uid'" : 'NULL'; | |
| 165 | ||
| 166 | if($id){ | |
| 167 | my $sql = qq~UPDATE `${database_name}`.automations SET name='$name_q', project_id=$pid_sql, is_active='$is_active', trigger_kind='$trigger', trigger_from_status=$from_sql, trigger_to_status=$to_sql, cond_priority=$cp_sql, action_kind='$action', action_priority=$ap_sql, action_status=$as_sql, action_user_id=$au_sql, action_comment_text='$text_q', action_webhook_url='$webhook_q' WHERE id='$id' AND company_id='$cid'~; | |
| 168 | $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__); | |
| 169 | $flash = "Automation <strong>$name_q</strong> saved."; | |
| 170 | }else{ | |
| 171 | my $sql = qq~INSERT INTO `${database_name}`.automations (company_id, project_id, name, is_active, trigger_kind, trigger_from_status, trigger_to_status, cond_priority, action_kind, action_priority, action_status, action_user_id, action_comment_text, action_webhook_url, created_by_user_id, created_at) VALUES ('$cid', $pid_sql, '$name_q', '$is_active', '$trigger', $from_sql, $to_sql, $cp_sql, '$action', $ap_sql, $as_sql, $au_sql, '$text_q', '$webhook_q', '$uid', NOW())~; | |
| 172 | $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__); | |
| 173 | $flash = "Automation <strong>$name_q</strong> created."; | |
| 174 | } | |
| 175 | } | |
| 176 | }elsif($act eq 'toggle_active'){ | |
| 177 | my $id = MODS::PTMatrix::PM::safe_int($form->{id}); | |
| 178 | $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.automations SET is_active=1-is_active WHERE id='$id' AND company_id='$cid'~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 179 | $flash = 'Toggled.'; | |
| 180 | }elsif($act eq 'delete'){ | |
| 181 | my $id = MODS::PTMatrix::PM::safe_int($form->{id}); | |
| 182 | $db->db_readwrite($dbh, qq~DELETE FROM `${database_name}`.automations WHERE id='$id' AND company_id='$cid'~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 183 | $flash = 'Deleted.'; | |
| 184 | } | |
| 185 | ||
| 186 | $db->db_disconnect($dbh); | |
| 187 | &automations_list($flash); | |
| 188 | } | |
| 189 | ||
| 190 | sub automations_list{ | |
| 191 | my($flash) = @_; | |
| 192 | $flash ||= ''; | |
| 193 | ||
| 194 | my $cid = MODS::PTMatrix::PM::get_company_id($userinfo); | |
| 195 | my $dbh = $db->db_connect(); | |
| 196 | ||
| 197 | #------------------------------------------------------------------------------------------------------ | |
| 198 | #Active rules first, newest first | |
| 199 | my $sql_r = qq~SELECT a.*, p.name AS project_name, p.key_prefix, au.display_name AS action_user_name FROM `${database_name}`.automations a LEFT JOIN `${database_name}`.projects p ON p.id = a.project_id LEFT JOIN `${database_name}`.users au ON au.id = a.action_user_id WHERE a.company_id='$cid' ORDER BY a.is_active DESC, a.id DESC~; | |
| 200 | my @rules = $db->db_readwrite_multiple($dbh, $sql_r, $ENV{SCRIPT_NAME}, __LINE__); | |
| 201 | ||
| 202 | #All projects for the per-rule filter dropdown | |
| 203 | my @projects = $db->db_readwrite_multiple($dbh, qq~SELECT id, name, key_prefix FROM `${database_name}`.projects WHERE company_id='$cid' ORDER BY name~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 204 | ||
| 205 | #Active users for the assign-to dropdown | |
| 206 | my @users = $db->db_readwrite_multiple($dbh, qq~SELECT id, display_name, email FROM `${database_name}`.users WHERE company_id='$cid' AND account_status='active' ORDER BY display_name~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 207 | #------------------------------------------------------------------------------------------------------ | |
| 208 | ||
| 209 | $db->db_disconnect($dbh); | |
| 210 | ||
| 211 | #Decorate each rule with its pre-rendered select-option HTML so the template | |
| 212 | #doesn't have to do template-side iteration of pickers (which the loop engine | |
| 213 | #handles awkwardly inside nested loops) | |
| 214 | foreach my $r(@rules){ | |
| 215 | $r->{name} = MODS::PTMatrix::PM::h($r->{name}); | |
| 216 | $r->{action_comment_text} = MODS::PTMatrix::PM::h($r->{action_comment_text} || ''); | |
| 217 | $r->{action_webhook_url} = MODS::PTMatrix::PM::h($r->{action_webhook_url} || ''); | |
| 218 | $r->{project_display} = $r->{project_name} ? MODS::PTMatrix::PM::h("$r->{key_prefix} $r->{project_name}") : 'any project'; | |
| 219 | $r->{toggle_label} = $r->{is_active} ? 'Disable' : 'Enable'; | |
| 220 | $r->{active_attr} = $r->{is_active} ? 'checked' : ''; | |
| 221 | $r->{project_options_html} = &proj_opts(\@projects, $r->{project_id}); | |
| 222 | $r->{trigger_options_html} = &opts($TRIGGERS, $r->{trigger_kind}); | |
| 223 | $r->{action_options_html} = &opts($ACTIONS, $r->{action_kind}); | |
| 224 | $r->{from_options_html} = &opts($STATUSES, $r->{trigger_from_status}, '— any —'); | |
| 225 | $r->{to_options_html} = &opts($STATUSES, $r->{trigger_to_status}, '— any —'); | |
| 226 | $r->{cond_prio_options_html} = &opts($PRIORITIES, $r->{cond_priority}, '— any —'); | |
| 227 | $r->{action_prio_options_html} = &opts($PRIORITIES, $r->{action_priority}, ''); | |
| 228 | $r->{action_status_options_html} = &opts($STATUSES, $r->{action_status}, ''); | |
| 229 | $r->{user_options_html} = &user_opts(\@users, $r->{action_user_id}); | |
| 230 | ||
| 231 | $r->{trigger_label} = ''; | |
| 232 | for my $t(@$TRIGGERS){$r->{trigger_label} = $t->[1] if $t->[0] eq $r->{trigger_kind};} | |
| 233 | $r->{action_label} = ''; | |
| 234 | for my $a(@$ACTIONS){$r->{action_label} = $a->[1] if $a->[0] eq $r->{action_kind};} | |
| 235 | } | |
| 236 | ||
| 237 | #Empty-rule pickers for the "create new" form | |
| 238 | my $new_proj_opts = &proj_opts(\@projects, 0); | |
| 239 | my $new_user_opts = &user_opts(\@users, 0); | |
| 240 | my $new_trigger_opts = &opts($TRIGGERS, ''); | |
| 241 | my $new_action_opts = &opts($ACTIONS, ''); | |
| 242 | my $new_from_opts = &opts($STATUSES, '', '— any —'); | |
| 243 | my $new_to_opts = &opts($STATUSES, '', '— any —'); | |
| 244 | my $new_cond_prio_opts = &opts($PRIORITIES, '', '— any —'); | |
| 245 | my $new_action_prio_opts = &opts($PRIORITIES, ''); | |
| 246 | my $new_action_status_opts = &opts($STATUSES, ''); | |
| 247 | ||
| 248 | #Create all template variables | |
| 249 | my $tvars; | |
| 250 | $tvars->{flash} = $flash; | |
| 251 | $tvars->{has_flash} = length($flash) ? 1 : 0; | |
| 252 | $tvars->{rules} = \@rules; | |
| 253 | $tvars->{has_rules} = scalar(@rules) ? 1 : 0; | |
| 254 | $tvars->{has_projects} = scalar(@projects) ? 1 : 0; | |
| 255 | $tvars->{has_users} = scalar(@users) ? 1 : 0; | |
| 256 | $tvars->{new_proj_opts} = $new_proj_opts; | |
| 257 | $tvars->{new_user_opts} = $new_user_opts; | |
| 258 | $tvars->{new_trigger_opts} = $new_trigger_opts; | |
| 259 | $tvars->{new_action_opts} = $new_action_opts; | |
| 260 | $tvars->{new_from_opts} = $new_from_opts; | |
| 261 | $tvars->{new_to_opts} = $new_to_opts; | |
| 262 | $tvars->{new_cond_prio_opts} = $new_cond_prio_opts; | |
| 263 | $tvars->{new_action_prio_opts} = $new_action_prio_opts; | |
| 264 | $tvars->{new_action_status_opts} = $new_action_status_opts; | |
| 265 | ||
| 266 | my $body = join('', $tfile->template('tf_automations.html', $tvars, $userinfo)); | |
| 267 | ||
| 268 | $load->render({ | |
| 269 | userinfo => $userinfo, | |
| 270 | page_key => 'automations', | |
| 271 | title => 'Automations', | |
| 272 | body => $body, | |
| 273 | }); | |
| 274 | } |