├── README.md ├── api ├── SampleMigration.sql └── AlertLoggerController.php ├── LICENSE └── alert.js /README.md: -------------------------------------------------------------------------------- 1 | # Receive Bank Alerts As Slack Notification 2 | 3 | Receive Bank Alerts As Slack Notifications. Compatible with GTBank, First Bank, Zenith Bank, UBA & Skye Bank. You need to activate email bank alerts. 4 | 5 | ### Installation 6 | 7 | * Create a new Google script and create a cron for it using the Google Scripts application. 8 | * Create an endpoint using a language of your choice and add the url to `alert.js`. It should log alerts, and return an array of new alerts. If you use **Laravel** we added a sample controller and a simple database schema you can use. It is meant to give you an idea on how to implement the endpoint. 9 | -------------------------------------------------------------------------------- /api/SampleMigration.sql: -------------------------------------------------------------------------------- 1 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; 2 | 3 | CREATE TABLE IF NOT EXISTS `alert` ( 4 | `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT, 5 | `google_email_id` varchar(64) DEFAULT NULL, 6 | `account_name` varchar(255) DEFAULT NULL, 7 | `account_number` varchar(15) DEFAULT NULL, 8 | `amount` decimal(10,0) DEFAULT NULL, 9 | `created_on` timestamp NULL DEFAULT NULL, 10 | `remark` text, 11 | `type` varchar(10) DEFAULT NULL, 12 | `bank_name` varchar(255) DEFAULT NULL, 13 | `has_transactions` int(11) NOT NULL, 14 | PRIMARY KEY (`id`) 15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; 16 | -------------------------------------------------------------------------------- /api/AlertLoggerController.php: -------------------------------------------------------------------------------- 1 | get('data'); 9 | 10 | $upsertedAlerts = []; 11 | 12 | for ($i = 0; $i < count($post); $i++) { 13 | $alert = $post[$i]; 14 | 15 | $alert["created_on"] = strtotime(trim($alert["created_on"])); 16 | 17 | $alertObject = Alert::firstOrNew([ 18 | "google_email_id" => $alert['google_email_id'] 19 | ]); 20 | 21 | if( ! $alertObject->exists) { 22 | $response = $alertObject->create($alert); 23 | 24 | array_push($upsertedAlerts, $response); 25 | } 26 | } 27 | 28 | return $upsertedAlerts; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Hotels NG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /alert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration values. 3 | */ 4 | 5 | var slackBotUsername = 'Finance'; 6 | var slackChannel = '#notifications'; 7 | var slackToken = 'YOUR-SLACK-TOKEN-HERE'; 8 | var crossCheckApi = 'http://cross-check-endpoint-url.com/'; 9 | 10 | 11 | /** 12 | * Test if the Slack integration is working. 13 | * 14 | * @param {string} message 15 | * @return {void} 16 | */ 17 | function sendSlackNotification(message){ 18 | message = message || "*Testing Notifications*"; 19 | var response = UrlFetchApp.fetch("https://slack.com/api/chat.postMessage?", { 20 | method:"POST", 21 | payload:{ 22 | "text":message, 23 | "token":slackToken, 24 | "channel":slackChannel, 25 | "username":slackBotUsername, 26 | "icon_emoji":":chart_with_upwards_trend:" 27 | } 28 | }); 29 | } 30 | 31 | 32 | /** 33 | * Where all the magic happens. 34 | * 35 | * Parses the emails and gets the alert details received and 36 | * posts it to a Slack channel. 37 | * 38 | * @return {void} 39 | */ 40 | function mainFunction() { 41 | var slackPayload = {data:[]}; 42 | 43 | var postToSlack = ""; 44 | 45 | var searchQuery = 'subject:"GeNS Transaction Alert" OR '+ 46 | 'subject:"FirstBank Alert On Your Account" OR '+ 47 | 'subject:"(UBA ALERT)" OR '+ 48 | 'subject:"Transaction Notification for HOTEL BOOKING LTD" OR '+ 49 | 'subject:"Skye Bank - Credit Alert" OR '+ 50 | 'subject:"Skye Bank - Debit Alert"'; 51 | 52 | var messagesInThread = []; 53 | 54 | var threads = GmailApp.search(searchQuery, 0, 50); 55 | 56 | threads.forEach(function(thread) { 57 | Array.prototype.push.apply(messagesInThread, thread.getMessages()); 58 | }); 59 | 60 | var start = 0, stop = 100; 61 | 62 | var messagesCount = messagesInThread.length; 63 | 64 | var postToSlack = " Parsing " + (start + 1) + " to " + (stop) + " of " + messagesCount; 65 | 66 | messagesInThread = messagesInThread.slice(start, stop); 67 | 68 | messagesInThread.forEach(function(msg) { 69 | var subject = msg.getSubject(), 70 | sender = msg.getFrom(), 71 | messageid = msg.getId(), 72 | data = undefined; 73 | 74 | // Skye Bank 75 | if (~sender.indexOf("skyebankng")) { 76 | data = extractDetailsSkye(msg.getBody(), subject); 77 | } 78 | 79 | // First Bank 80 | else if (~sender.indexOf("firstbanknigeria")) { 81 | data = extractDetailsFirstBank(msg.getBody(), subject); 82 | } 83 | 84 | // UBA 85 | else if (~sender.indexOf("ubagroup")){ 86 | data = extractDetailsUBA(msg.getBody(), subject); 87 | } 88 | 89 | // GTBank 90 | else if (~sender.indexOf("gtbank")) { 91 | data = extractDetailsGTB(msg.getBody(), subject); 92 | } 93 | 94 | // Zenith Bank 95 | else if (~sender.indexOf("zenithbank")) { 96 | data = extractDetailsZenith(msg.getBody(), subject); 97 | } 98 | 99 | formatSlackMessage(data, messageid); 100 | }); 101 | 102 | sendToEndpoint(slackPayload); 103 | 104 | /** 105 | * Send to the endpoint. 106 | * 107 | * @param {Object} payload 108 | * @return {void} 109 | */ 110 | function sendToEndpoint(payload) { 111 | payload = {data:JSON.stringify(payload.data)}; 112 | 113 | var response = UrlFetchApp.fetch(crossCheckApi, {method:"POST", payload:payload}); 114 | 115 | var slackMessage = ""; 116 | var upsertedAlerts = JSON.parse(response.getContentText()); 117 | 118 | upsertedAlerts.forEach(function (ualert) { 119 | slackMessage += generateSlackMessage(ualert); 120 | }); 121 | 122 | if (slackMessage.length >= 1) { 123 | sendSlackNotification(slackMessage); 124 | } 125 | } 126 | 127 | 128 | /** 129 | * Generate the alert message to send to Slack. 130 | * 131 | * @param {Object} parsedObject 132 | * @return {string} 133 | */ 134 | function generateSlackMessage(parsedObject) { 135 | var message = ""; 136 | 137 | if (parsedObject.AccName !== "") { 138 | message += "\n*********\n" + 139 | "Bank: " + parsedObject.bank_name + "\n" + 140 | "Account Name: " + parsedObject.account_name + "\n" + 141 | "Transaction Type: " + parsedObject.type + "\n" + 142 | "Account Number: " + parsedObject.account_number + "\n" + 143 | "Amount: " + parsedObject.amount + "\n" + 144 | "Date: " + parsedObject.created_on + "\n" + 145 | "Details: " + parsedObject.remark + "\n" + 146 | "Message ID:" + parsedObject.google_email_id + "\n\n"; 147 | } 148 | 149 | return message; 150 | } 151 | 152 | 153 | // ------------------------------------------------------ 154 | // Formatting 155 | // ------------------------------------------------------ 156 | 157 | function stripExcessSpaces(text) { 158 | return text.replace(/\s\s\s*/g,''); 159 | } 160 | 161 | function stripTags(text){ 162 | if (text && text != "") { 163 | return stripExcessSpaces(text.replace(/<\w*?.*?>.*?<\/\w*?>/g,'').trim()); 164 | } 165 | 166 | return text; 167 | } 168 | 169 | function formatAmount(amount) { 170 | return amount.replace(/[a-zA-Z,]*/g,''); 171 | } 172 | 173 | function formatSlackMessage(transaction, id) { 174 | var pts = { 175 | google_email_id: id, 176 | account_name: stripTags(transaction.AccName), 177 | account_number: stripTags(transaction.AccNum), 178 | amount: formatAmount(stripTags(transaction.Amount)), 179 | created_on: stripTags(transaction.Date), 180 | remark: stripTags(transaction.Remark), 181 | type: transaction.TransType.trim(), 182 | bank_name: transaction.Bank, 183 | has_transactions: 0 184 | }; 185 | 186 | if (transaction.AccName !== "") { 187 | postToSlack += "\n*********\n" + 188 | "Bank: " + stripTags(transaction.Bank) + "\n" + 189 | "Account Name: " + stripTags(transaction.AccName) + "\n" + 190 | "Transaction Type: " + stripTags(transaction.TransType.trim()) + "\n" + 191 | "Account Number: " + stripTags(transaction.AccNum) + "\n" + 192 | "Amount: " + stripTags(transaction.Amount) + "\n" + 193 | "Date: " + stripTags(transaction.Date) + "\n" + 194 | "Details: " + stripTags(transaction.Remark) + "\n" + 195 | "Message ID:" + stripTags(id) + "\n\n"; 196 | 197 | slackPayload.data.push(pts); 198 | } 199 | } 200 | 201 | // ------------------------------------------------------ 202 | // Extract Alert Details 203 | // ------------------------------------------------------ 204 | 205 | 206 | /** 207 | * Extract details for GTBank 208 | * 209 | * @param {string} emailBody 210 | * @param {string} emailSubject 211 | * @return {Object} 212 | */ 213 | function extractDetailsGTB(emailBody, emailSubject) { 214 | var dataForExtraction = [ 215 | "Account Number", 216 | "Transaction Location", 217 | "Description", 218 | "Amount", 219 | "Value Date", 220 | "Remarks", 221 | "Time of Transaction" 222 | ]; 223 | 224 | emailBody = emailBody.replace(/\r?\n|\r||<\/span>|

<\/h1>/g,''); 225 | 226 | var pattern_start = "\\s*"; 227 | var pattern_end = "\\s*<\/TD>\\s*\\s*:\\s*<\/TD>\\s*(.*?)\\s*<\/TD>"; 228 | 229 | var parsed = {}; 230 | var parseRegEx; 231 | 232 | dataForExtraction.forEach(function(e) { 233 | parseRegEx = new RegExp(pattern_start + e + pattern_end); 234 | var matched = emailBody.match(parseRegEx); 235 | parsed[e.replace(':', '')] = matched ? matched[1] : ""; 236 | }); 237 | 238 | var transactiontype = (~emailBody.indexOf('Debit')) ? "DEBIT" : "CREDIT"; 239 | 240 | return { 241 | "AccName":"N/A", 242 | "AccNum":parsed["Account Number"], 243 | "Amount":parsed["Amount"], 244 | "Date":parsed["Value Date"] + " " + parsed["Time of Transaction"], 245 | "Remark":parsed["Description"], 246 | "TransType":transactiontype, 247 | "Bank":"GTB" 248 | }; 249 | } 250 | 251 | /** 252 | * Extract details for UBA 253 | * 254 | * @param {string} emailBody 255 | * @param {string} emailSubject 256 | * @return {Object} 257 | */ 258 | function extractDetailsUBA(emailBody, emailSubject) { 259 | var dataForExtraction = [ 260 | "Transaction Type", 261 | "Transaction Amount", 262 | "Transaction Currency", 263 | "Account Number", 264 | "Transaction Narration", 265 | "Transaction Remarks", 266 | "Date and Time", 267 | "Account Name", 268 | "Cleared Balance", 269 | "Transaction Type" 270 | ]; 271 | 272 | emailBody = emailBody.replace(/\r?\n|\r/g,''); 273 | 274 | var pattern_start = ""; 275 | var pattern_end = "<\/td>\\s*(.*?)<\/td>"; 276 | 277 | var parsed = {}; 278 | var parseRegEx; 279 | 280 | dataForExtraction.forEach(function(e) { 281 | parseRegEx = new RegExp(pattern_start + e + pattern_end); 282 | var matched = emailBody.match(parseRegEx); 283 | parsed[e.replace(':', '')] = matched ? matched[1] : ""; 284 | }); 285 | 286 | return { 287 | "AccName":parsed["Account Name"], 288 | "AccNum":parsed["Account Number"], 289 | "Amount":parsed["Transaction Amount"], 290 | "Date":parsed["Date and Time"], 291 | "Remark":parsed["Transaction Narration"], 292 | "TransType":parsed["Transaction Type"], 293 | "Bank":"UBA" 294 | }; 295 | } 296 | 297 | /** 298 | * Extract details for Skye bank 299 | * 300 | * @param {string} emailBody 301 | * @param {string} emailSubject 302 | * @return {Object} 303 | */ 304 | function extractDetailsSkye(emailBody, emailSubject) { 305 | var dataForExtraction = [ 306 | "Account Name", 307 | "Account Number", 308 | "Amount", 309 | "Details", 310 | "Balance", 311 | "Time" 312 | ]; 313 | 314 | emailBody = emailBody.replace(/\r?\n|\r/g,''); 315 | 316 | var pattern_start = ""; 317 | var pattern_end = "\\s*<\/td>\\s*(.*?)\\s*<\/td>"; 318 | 319 | var parsed = {}; 320 | var parseRegEx; 321 | 322 | dataForExtraction.forEach(function(e) { 323 | parseRegEx = new RegExp(pattern_start + e + pattern_end); 324 | var matched = emailBody.match(parseRegEx); 325 | parsed[e.replace(':', '')] = matched ? matched[1] : ""; 326 | }); 327 | 328 | var transactiontype = (~emailBody.indexOf('Debit')) ? "DEBIT" : "CREDIT"; 329 | 330 | return { 331 | "AccName":parsed["Account Name"], 332 | "AccNum":parsed["Account Number"], 333 | "Amount":parsed["Amount"], 334 | "Date":parsed["Time"], 335 | "Remark":parsed["Details"], 336 | "TransType":transactiontype, 337 | "Bank":"Skye Bank" 338 | }; 339 | } 340 | 341 | /** 342 | * Extract details for First Bank 343 | * 344 | * @param {string} emailBody 345 | * @param {string} emailSubject 346 | * @return {Object} 347 | */ 348 | function extractDetailsFirstBank(emailBody, emailSubject) { 349 | var dataForExtraction = [ 350 | "Account Number:", 351 | "Amount:", 352 | "Transaction Narrative:", 353 | "Transaction Date:" 354 | ]; 355 | 356 | emailBody = emailBody.replace(/\r?\n|\r||<\/strike>|

<\/h1>/g,''); 357 | 358 | var pattern_start = "

"; 359 | var pattern_end = "\\s*

<\/td>\\s*

(.*?)\\s*

<\/td>"; 360 | 361 | var pattern = "

Account Number:\\s*

<\/td>\\s*

(.*?)\\s*

<\/td>"; 362 | 363 | var parsed = {}; 364 | var parseRegEx; 365 | 366 | dataForExtraction.forEach(function(e) { 367 | parseRegEx = new RegExp(pattern_start + e + pattern_end); 368 | var matched = emailBody.match(parseRegEx); 369 | parsed[e.replace(':', '')] = matched ? matched[1] : ""; 370 | }); 371 | 372 | var transactiontype = (~emailSubject.indexOf('DEBIT')) ? "DEBIT" : "CREDIT"; 373 | 374 | return { 375 | "AccName":"N/A", 376 | "AccNum":parsed["Account Number"], 377 | "Amount":parsed["Amount"], 378 | "Date":parsed["Transaction Date"], 379 | "Remark":parsed["Transaction Narrative"], 380 | "TransType":transactiontype, 381 | "Bank":"First Bank" 382 | }; 383 | } 384 | 385 | /** 386 | * Extract details for Zenith Bank 387 | * 388 | * @param {string} emailBody 389 | * @param {string} emailSubject 390 | * @return {Object} 391 | */ 392 | function extractDetailsZenith(emailBody, emailSubject) { 393 | var dataForExtraction = [ 394 | "Account Number", 395 | "Description", 396 | "Currency", 397 | "Amount", 398 | "Date of Transaction", 399 | "Trans. Type" 400 | ]; 401 | 402 | emailBody = emailBody.replace(/\r?\n|\r|
|<\/div>|<\/td>|  ||<\/strong>|

<\/h1>/g,''); 403 | 404 | var pattern_start = ""; 405 | var pattern_end = "<\/th>(.*?)<\/td>"; 406 | 407 | var parsed = {}; 408 | var parseRegEx; 409 | 410 | dataForExtraction.forEach(function(e) { 411 | parseRegEx = new RegExp(pattern_start + e + pattern_end); 412 | var matched = emailBody.match(parseRegEx); 413 | parsed[e] = matched ? matched[1] : ""; 414 | }); 415 | 416 | return { 417 | "AccName":"N/A", 418 | "AccNum":parsed["Account Number"], 419 | "Amount":parsed["Amount"], 420 | "Date":parsed["Date of Transaction"], 421 | "Remark":parsed["Description"], 422 | "TransType":parsed["Trans. Type"], 423 | "Bank":"Zenith Bank" 424 | }; 425 | } 426 | } 427 | --------------------------------------------------------------------------------