├── gapps.config.json ├── .gitignore ├── src ├── .gitignore ├── gas-freshdesk-tests.js └── gas-freshdesk-lib.js ├── package.json ├── LICENSE ├── scripts └── pull.sh └── README.md /gapps.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "src", 3 | "fileId": "Mta4oea1VMIugfSGRo4QrAnKRT9d30hqB" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | npm-debug.log 3 | node_modules 4 | secret.json 5 | gapps.config.json.bak 6 | t.js 7 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | gasl-demo.js 2 | gas-log-lib.js 3 | gas-tap-lib.js 4 | gast-tests.js 5 | debug.js 6 | gas-gmail-channel-lib.js 7 | gas-gmail-channel-tests.js 8 | polyfill.js 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gas-freshdesk" 3 | , "version": "0.1.0" 4 | , "description": "GasFreshdesk is a wrapper class for Freshdesk API in GAS(Google Apps Script). It provides a easy way to deal with tickets in GAS" 5 | , "main": "index.js" 6 | , "directories": { 7 | "doc": "doc" 8 | } 9 | , "dependencies": { 10 | } 11 | , "devDependencies": { 12 | "node-google-apps-script": "1.1.3" 13 | } 14 | , "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | , "pull": "scripts/pull.sh" 17 | } 18 | , "repository": { 19 | "type": "git" 20 | , "url": "git@github.com:zixia/gas-freshdesk.git" 21 | } 22 | , "keywords": [ 23 | "freshdesk" 24 | , "google-apps-script" 25 | , "gas" 26 | ] 27 | , "author": "Zhuohuan LI " 28 | , "license": "MIT" 29 | , "homepage": "https://github.com/zixia/gas-freshdesk" 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Zhuohuan LI 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 | 23 | -------------------------------------------------------------------------------- /scripts/pull.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 4 | # 5 | # 6 | # 7 | # 8 | 9 | GIT_STATUS=$( git status -s | wc -l ) 10 | 11 | [ "$1" = "force" ] && GIT_STATUS=0 12 | 13 | [ "$GIT_STATUS" -gt 0 ] && { 14 | git status 15 | echo 16 | echo "##################################################" 17 | echo "there's local changes in git not commited(pushed)!" 18 | echo "##################################################" 19 | echo 20 | echo "run 'git commit -a' to fix it. (also suggest to run a 'git push' after that)" 21 | echo 22 | exit 23 | } 24 | 25 | BASE_DIR=$(cd "$(dirname "$0")"; pwd) 26 | 27 | GAPPS_CONFIG="$BASE_DIR/../gapps.config.json" 28 | GAPPS_CMD="$BASE_DIR/../node_modules/.bin/gapps" 29 | 30 | [ -e $GAPPS_CONFIG ] || { 31 | echo "GAPPS_CONFIG not found!" 32 | exit 255 33 | } 34 | 35 | [ -e $GAPPS_CMD ] || { 36 | echo "GAPPS_CMD not exist!" 37 | echo "npm install first" 38 | exit 255 39 | } 40 | 41 | fileId=$(grep fileId "$GAPPS_CONFIG" | cut -d'"' -f4) 42 | [ -n "$fileId" ] || { 43 | echo "fileId not found!" 44 | exit 255 45 | } 46 | 47 | # 48 | # 49 | # 50 | echo -n "Start pulling GAS script id: $fileId from ggoogle drive... " 51 | 52 | mv -f "$GAPPS_CONFIG" "${GAPPS_CONFIG}.bak" 53 | 54 | cd "$BASE_DIR/.." 55 | $GAPPS_CMD clone $fileId 56 | 57 | # 58 | # 59 | # 60 | echo "Done." 61 | 62 | 63 | # 64 | # Remove .gitignore files in src 65 | # 66 | echo -n "Removing useless files in src directory... " 67 | path=$(grep path "$GAPPS_CONFIG" | cut -d'"' -f4) 68 | [ -n "$path" ] || { 69 | echo "path not found!" 70 | exit 255 71 | } 72 | 73 | for uselessFile in $( cat $BASE_DIR/../$path/.gitignore ); do 74 | echo -n " $uselessFile " 75 | rm -f $BASE_DIR/../$path/$uselessFile 76 | done 77 | 78 | echo "Done." 79 | 80 | 81 | echo 82 | 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GasFreshdesk - OO Class for Freshdesk API(v2) 2 | GasFreshdesk is a simple Freshdesk API Class for GAS(Google Apps Script) 3 | 4 | Github: https://github.com/huan/gas-freshdesk 5 | 6 | ## Why GasFreshdesk? 7 | 8 | I made a number of freshdesk API calls in GAS in order to convert emails from my Gmail account to tickets in my daily life. Most of the emails are business plans with attachments. 9 | 10 | I found Freshdesk API very hard to use and debug in GAS, especialy with attachments. Writing more code became more complicated. So I decided to modulize it to GasFreshdesk module. 11 | 12 | ### What does GasFreshdesk look like? 13 | 14 | GasFreshdesk is very clean and easy to use. 15 | 16 | ```javascript 17 | var MyFreshdesk = new GasFreshdesk('https://mikebo.freshdesk.com', 'Jrg0FQNzX3tzuHbiFjYQ') 18 | 19 | var ticket = new MyFreshdesk.Ticket({ 20 | description:'A description' 21 | , subject: 'A subject' 22 | , email: 'you@example.com' 23 | , attachments: [ Utilities.newBlob('TEST DATA').setName('test-data.dat') ] 24 | }) 25 | 26 | ticket.assign(9000658396) 27 | ticket.note({ 28 | body: 'Hi tom, Still Angry' 29 | , private: true 30 | }) 31 | ticket.reply({ 32 | body: 'Hi tom, Still Angry' 33 | , cc_emails: ['you@example.com'] 34 | }) 35 | ticket.setPriority(2) 36 | ticket.setStatus(2) 37 | 38 | ticket.del() 39 | ticket.restore() 40 | 41 | Logger.log('ticket #' + ticket.getId() + ' was set!') 42 | ``` 43 | 44 | It's very clean and easy to use, huh ;-) 45 | 46 | ### How to enable GasFreshdesk library in GAS? 47 | 48 | To use GasFreshdesk in your GAS script editor, just add the follow lines, then you are set. 49 | 50 | ```javascript 51 | if ((typeof GasFreshdesk)==='undefined') { // GasFreshdesk Initialization. (only if not initialized yet.) 52 | eval(UrlFetchApp.fetch('https://raw.githubusercontent.com/zixia/gas-freshdesk/master/src/gas-freshdesk-lib.js').getContentText()) 53 | } // Class GasFreshdesk is ready for use now! 54 | 55 | ``` 56 | 57 | ## API 58 | 59 | Gas Freshdesk Library use [API v2](http://developer.freshdesk.com/api/) to communicate with freshdesk. 60 | 61 | The old version use Freshdesk API v1 is [Gas Freshdesk Library v0.2.0](#v0.2.0). 62 | 63 | ### 1. Class `GasFreshdesk` 64 | 65 | 66 | #### 1.1 `GasFreshdesk(url, key)`: Class constructor for Freshdesk 67 | 68 | Return MyFreshdesk for you. 69 | 70 | ```javascript 71 | var MyFreshdesk = new GasFreshdesk( 72 | 'YOUR_FRESHDESK_URL_HERE' // i.e. https://mikebo.freshdesk.com 73 | , 'YOUR_API_KEY_HERE' 74 | ) 75 | ``` 76 | 77 | #### 1.2 `MyFreshdesk.listTickets(options)` 78 | 79 | List or search for tickets. Return a array of instances of Ticket. 80 | 81 | * `options.email`: email address of requester 82 | * `options.requester_id`: requester_id of requester 83 | 84 | if `options` is not provided, then listTickets will uses the new_and_my_open filter. 85 | 86 | ```javascript 87 | var tickets = MyFreshdesk.listTickets({ email: 'you@example.com' }) 88 | var tickets = MyFreshdesk.listTickets({ requester_id: 4312412413 }) 89 | ``` 90 | 91 | ### 2. Class `Ticket` 92 | 93 | #### 2.1 `MyFreshdesk.Ticket({...})`: Class constructor for Ticket 94 | 95 | Create a new ticket for you. 96 | 97 | ```javascript 98 | var ticket = new MyFreshdesk.Ticket({ 99 | description:'A totally rad description of a what the problem is' 100 | , subject:'Something like "Cannot log in"' 101 | , email: 'you@example.com' 102 | , attachments: [ 103 | Utilities.newBlob('TEST DATA').setName('test-data.dat') 104 | , Utilities.newBlob('TEST2 DATA').setName('test-data2.dat') 105 | ] 106 | }) 107 | ``` 108 | 109 | #### 2.2 `MyFreshdesk.Ticket(id)`: Class constructor for Ticket 110 | 111 | Load a existing ticket for you. 112 | 113 | ```javascript 114 | var ticket = new MyFreshdesk.Ticket(1) 115 | ``` 116 | 117 | #### 2.3 `Ticket.del()`: Delete ticket 118 | 119 | ```javascript 120 | ticket.del() 121 | ``` 122 | 123 | #### 2.4 `Ticket.list()`: List tickets 124 | 125 | 126 | #### 2.5 `Ticket.setStatus()` 127 | 128 | Set ticket to status with parameter. 129 | 130 | Has the following shortcut methods: 131 | 132 | 1. `open()` 133 | 1. `pend()` 134 | 1. `resolv()` 135 | 1. `close()` 136 | 137 | #### 2.6 `Ticket.note()` 138 | 139 | Note a ticket. 140 | 141 | ```javascript 142 | ticket.note({ 143 | body: 'Hi tom, Still Angry' 144 | , private: true 145 | }) 146 | ``` 147 | 148 | #### 2.7 `Ticket.reply()` 149 | 150 | Reply a ticket. 151 | 152 | ```javascript 153 | ticket.reply({ 154 | body: 'Hi tom, Still Angry' 155 | , cc_emails: ['you@example.com'] 156 | }) 157 | ``` 158 | ### 3. Class `Contact` 159 | 160 | ```javascript 161 | var requesterId = ticket.getRequesterId() 162 | var contact = new MyFreshdesk.Contact(requesterId) 163 | 164 | Logger.log(contact.getEmail()) 165 | ``` 166 | 167 | #### 3.1 `Contact.list()`: List contacts 168 | 169 | Search contacts by email. 170 | 171 | ```javascript 172 | var Contact = GasFreshdesk.Contact 173 | var contacts = Contact.list({ email: 'you@example.com' }) 174 | Logger.log(contacts[0].getName()) 175 | ``` 176 | 177 | ### 4. Class `Agent` 178 | 179 | TBW 180 | 181 | #### 4.1 `Agent.list(options)`: List agents 182 | 183 | Search for agent. 184 | 185 | * options.email email of agent 186 | 187 | ```javascript 188 | var Agent = GasFreshdesk.Agent 189 | var agents = Agent.list({ email: 'you@example.com' }) 190 | Logger.log(agents[0].getId()) 191 | ``` 192 | 193 | ## Test Suites 194 | 195 | There is a test suite that comes with GasFreshdesk, which uses [GasTap](https://github.com/zixia/gast), a tap testing-framework for GAS. 196 | 197 | More sample code could be found in the test files if you like to look into it. 198 | 199 | GasFreshdesk test suite: https://github.com/zixia/gas-freshdesk/blob/master/src/gas-freshdesk-tests.js 200 | 201 | ### How to run tests? 202 | 203 | You must run tests inside google apps script editor. Open google script editor, copy/paste gas-freshdesk-tests.js into it, then click Run. 204 | 205 | There's also an easier way to do it, you could go to my develop environment(read only) to run and clone. Follow this link: https://script.google.com/a/zixia.net/macros/d/Mta4oea1VMIugfSGRo4QrAnKRT9d30hqB/edit?uiv=2&mid=ACjPJvGt4gnXjJwXnToB0jIMEbSvqKUF6vH-uq-m59SqnjXqTQ03NDn_khlNE6ha_mPnrOAYEnyFk80nHYmt_hppO3AgDkO_vVLrYJXzcPPagwRromd0znfLreNFAu4p0rYTC-Jlo-sAKOM, then click `gas-freshdesk-test.gs` in file browser on the left. 206 | 207 | ## Support 208 | 209 | The GasFreshdesk source code repository is hosted on GitHub. There you can file bugs on the issue tracker or submit tested pull requests for review. ( https://github.com/zixia/gas-freshdesk/issues ) 210 | 211 | For real-world examples from open-source projects using GasL, see Projects Using TasL on the wiki. ( https://github.com/zixia/gas-freshdesk/wiki ) 212 | 213 | ## HISTORY 214 | 215 | ### [v0.3.0(January 20, 2016)](https://github.com/zixia/gas-freshdesk/releases/tag/v0.3.0) 216 | * Switch to [Freshdesk API v2](http://developer.freshdesk.com/api/) 217 | * Change library name from`Freshdesk` to `GasFreshdesk` 218 | * Added new method: Ticket.reply() 219 | 220 | To use the v0.3.0 gas-freshdesk library, put the following snip in your gas code. 221 | 222 | ```javascript 223 | if ((typeof Freshdesk)==='undefined') { // GasFreshdesk Initialization. (only if not initialized yet.) 224 | eval(UrlFetchApp.fetch('https://raw.githubusercontent.com/zixia/gas-freshdesk/v0.3.0/src/gas-freshdesk-lib.js').getContentText()) 225 | } // Class GasFreshdesk is ready for use now! 226 | ``` 227 | 228 | ### [v0.2.0(January 11, 2016)](https://github.com/zixia/gas-freshdesk/releases/tag/v0.2.0) 229 | * Last stable version with [Freshdesk API v1](https://freshdesk.com/api) 230 | * [v0.2.0 README](https://github.com/zixia/gas-freshdesk/blob/v0.2.0/README.md) 231 | * [v0.2.0 Test Suite](https://github.com/zixia/gas-freshdesk/blob/v0.2.0/src/gas-freshdesk-tests.js) 232 | 233 | To use the v0.2.0 gas-freshdesk library with freshdesk api v1, put the following snip in your gas code. 234 | 235 | ```javascript 236 | if ((typeof Freshdesk)==='undefined') { // GasFreshdesk Initialization. (only if not initialized yet.) 237 | eval(UrlFetchApp.fetch('https://raw.githubusercontent.com/zixia/gas-freshdesk/v0.2.0/src/gas-freshdesk-lib.js').getContentText()) 238 | } // Class GasFreshdesk is ready for use now! 239 | ``` 240 | 241 | 242 | ### v0.1.0(December 16, 2015) 243 | * Initial public release. 244 | 245 | ## AUTHOR 246 | 247 | [Huan LI (李卓桓)](http://linkedin.com/in/zixia) \ 248 | 249 | 250 | profile for zixia on Stack Exchange, a network of free, community-driven Q&A sites 251 | 252 | 253 | ## COPYRIGHT & LICENSE 254 | 255 | - Code & Docs © 2015-now Huan LI \ 256 | - Code released under MIT-style license; see LICENSE for details. 257 | - Docs released under Creative Commons 258 | -------------------------------------------------------------------------------- /src/gas-freshdesk-tests.js: -------------------------------------------------------------------------------- 1 | // if ((typeof GasLog)==='undefined') { // GasL Initialization. (only if not initialized yet.) 2 | // eval(UrlFetchApp.fetch('https://raw.githubusercontent.com/zixia/gasl/master/src/gas-log-lib.js').getContentText()) 3 | // } // Class GasLog is ready for use now! 4 | // var log = new GasLog() 5 | 6 | function freshdeskTestRunner() { 7 | 'use strict' 8 | 9 | /** 10 | * 11 | * GasFreshdesk - Freshdesk API Class for Google Apps Script 12 | * 13 | * GasFreshdesk is a easy to use Freshdesk API Class for GAS(Google Apps Script) 14 | * It provides a OO(Object-Oriented) way to use Freshdesk Ticket / Contacts, etc. 15 | * 16 | * Github - https://github.com/zixia/gas-freshdesk 17 | * 18 | * Example: 19 | ```javascript 20 | var MyFreshdesk = new GasFreshdesk('https://mikebo.freshdesk.com', 'Jrg0FQNzX3tzuHbiFjYQ') 21 | 22 | var ticket = new MyFreshdesk.Ticket({ 23 | description:'A description' 24 | , subject: 'A subject' 25 | , email: 'you@example.com' 26 | }) 27 | 28 | ticket.assign(9000658396) 29 | ticket.note({ 30 | body: 'Hi tom, Still Angry' 31 | , private: true 32 | }) 33 | ticket.setPriority(2) 34 | ticket.setStatus(2) 35 | 36 | ticket.del() 37 | ticket.restore() 38 | ``` 39 | */ 40 | 41 | if ((typeof GasLog)==='undefined') { // GasL Initialization. (only if not initialized yet.) 42 | eval(UrlFetchApp.fetch('https://raw.githubusercontent.com/zixia/gasl/master/src/gas-log-lib.js').getContentText()) 43 | } // Class GasLog is ready for use now! 44 | var log = new GasLog() 45 | 46 | if ((typeof GasTap)==='undefined') { // GasT Initialization. (only if not initialized yet.) 47 | eval(UrlFetchApp.fetch('https://raw.githubusercontent.com/zixia/gast/master/src/gas-tap-lib.js').getContentText()) 48 | } // Class GasTap is ready for use now! 49 | var test = new GasTap() 50 | 51 | // This is my test account, don't worry, thanks. ;] 52 | var FRESHDESK_URL = 'https://mikebo.freshdesk.com' 53 | 54 | // Sorry for this maybe make some people(hope not include you) who hate show plain text secret(key/password) in code. 55 | // This is the key for agent 'zixia@zixia.net' at 'https://mikebo.freshdesk.com', just for easy testing... 56 | var FRESHDESK_KEY = 'Jrg0FQNzX3tzuHbiFjYQ' 57 | 58 | /****************************************************************** 59 | */ 60 | 61 | // return development() 62 | // return (testSearch() + test.finish()) 63 | // return (testFreshdeskTicket() + test.finish()) 64 | 65 | /* 66 | *******************************************************************/ 67 | 68 | 69 | /****************************************************************** 70 | * 71 | * Test cases 72 | * 73 | */ 74 | 75 | testHttpBackend() 76 | testUtils() 77 | 78 | testValidators() 79 | 80 | testFreshdeskAuth() 81 | 82 | testFreshdeskTicket() 83 | testFreshdeskContact() 84 | testFreshdeskAgent() 85 | 86 | testSearch() 87 | 88 | test.finish() 89 | 90 | //////////////////////////////////////////////////////////////////////// 91 | 92 | function development() { 93 | test('Ticket', function (t) { 94 | var TICKET_ID = 1 95 | // ??? var EXPECTED_ID = 9000658396 // Agent ID of Mike@zixia.net 96 | 97 | var MyFreshdesk = new GasFreshdesk(FRESHDESK_URL, FRESHDESK_KEY) 98 | var oldTicket = new MyFreshdesk.Ticket(TICKET_ID) 99 | }) 100 | 101 | } 102 | 103 | 104 | function testSearch() { 105 | var MyFreshdesk = new GasFreshdesk(FRESHDESK_URL, FRESHDESK_KEY) 106 | 107 | test('listTickets', function (t) { 108 | var EMAIL = 'you@example.com' 109 | 110 | tickets = MyFreshdesk.listTickets({ email: EMAIL }) 111 | 112 | t.ok(tickets.length, 'get listTickets result') 113 | t.ok(tickets[0].getId(), 'ticket id valid') 114 | 115 | var contactId = tickets[0].getRequesterId() 116 | var contact = new MyFreshdesk.Contact(contactId) 117 | 118 | t.equal(contact.getEmail(), EMAIL, 'contact email match') 119 | 120 | var ticketId = tickets[0].getId() 121 | tickets = MyFreshdesk.listTickets({ requester_id: contactId }) 122 | t.ok(tickets.length, 'listTickets by requester_id') 123 | t.equal(tickets[0].getId(), ticketId, 'search by requester_id') 124 | 125 | // XXX 126 | // var EMAIL_NEED_ENCODE = 'you+owner@example.com' 127 | // t.notThrow(function () { 128 | // MyFreshdesk.listTickets({ email: EMAIL_NEED_ENCODE }) 129 | // }, 'search email include "+"') 130 | }) 131 | 132 | test('listContacts', function (t) { 133 | var EMAIL = 'you@example.com' 134 | contacts = MyFreshdesk.listContacts({ email: EMAIL }) 135 | 136 | t.ok(contacts.length, 'get listContacts result') 137 | t.ok(contacts[0].getId(), 'contact id valid') 138 | }) 139 | } 140 | 141 | function testFreshdeskAgent() { 142 | 143 | test ('Agent', function (t) { 144 | var EMAIL = 'zixia@zixia.net' 145 | 146 | var MyFreshdesk = new GasFreshdesk(FRESHDESK_URL, FRESHDESK_KEY) 147 | 148 | var agents = MyFreshdesk.listAgents({ email: EMAIL }) 149 | 150 | t.ok(agents.length, 'lsitAgents') 151 | 152 | var agent = agents[0] 153 | 154 | t.ok(agent.getName(), 'agent has name') 155 | t.ok(agent.getId(), 'agent has id') 156 | }) 157 | } 158 | 159 | function testFreshdeskContact() { 160 | 161 | test ('Contact', function (t) { 162 | var EMAIL = 'you@example.com' 163 | var EXPECTED_NAME = 'expected name' 164 | 165 | var MyFreshdesk = new GasFreshdesk(FRESHDESK_URL, FRESHDESK_KEY) 166 | 167 | var contacts = MyFreshdesk.listContacts({ email: EMAIL }) 168 | t.ok(contacts, 'contact list') 169 | 170 | var contact = contacts[0] 171 | t.ok(contact.getName(), 'contact has name') 172 | contact.setName(EXPECTED_NAME) 173 | t.equal(contact.getName(), EXPECTED_NAME, 'contact name as expected') 174 | 175 | contact.setName('You You') 176 | 177 | t.ok(contact.getId(), 'contact has id') 178 | }) 179 | 180 | } 181 | 182 | function testValidators() { 183 | 184 | test ('Validate Ticket Object', function (t) { 185 | 186 | var OK_TICKET_OBJ = { 187 | email: 'email@email.com' 188 | } 189 | 190 | var OK_TICKET_WITH_ATT_OBJ = { 191 | email: 'email@email.com' 192 | , attachments: [ 193 | 'blob' 194 | ] 195 | } 196 | 197 | var OK_TICKET_EMAIL_OBJ = { 198 | email: 'test_.-email@email_em-ail.co.jp' 199 | } 200 | 201 | var NOT_OK_TICKET_EMAIL_OBJ = { 202 | email: 'n@t a valid address' 203 | } 204 | 205 | t.notThrow(function () { GasFreshdesk.validateHelpdeskObject(OK_TICKET_OBJ) }, 'ticket obj with right key') 206 | 207 | t.notThrow(function () { GasFreshdesk.validateHelpdeskObject(OK_TICKET_EMAIL_OBJ) }, 'ticket obj with valid email address') 208 | t.throws(function () { GasFreshdesk.validateHelpdeskObject(NOT_OK_TICKET_EMAIL_OBJ) }, 'ticket obj with invalid email address') 209 | 210 | t.throws(function () { GasFreshdesk.validateInteger('a') }, 'validateInteger a string') 211 | t.notThrow(function () { GasFreshdesk.validateInteger(1) }, 'validateInteger a integer') 212 | }) 213 | } 214 | 215 | function testUtils() { 216 | 217 | test ('Attachment Helper', function (t) { 218 | var HAS_ATTACHMENT = { 219 | a: { 220 | b: { 221 | attachments: [1,2] 222 | } 223 | } 224 | } 225 | 226 | var NO_ATTACHMENT = { 227 | a: { 228 | b: { 229 | haha: [1,2] 230 | } 231 | } 232 | } 233 | 234 | var http = GasFreshdesk.Http('a','b') 235 | 236 | var hasAtt = http.hasAttachment(HAS_ATTACHMENT) 237 | t.ok(hasAtt, 'HAS_ATTACHMENT has attachment') 238 | 239 | var noAtt = http.hasAttachment(NO_ATTACHMENT) 240 | t.ok(!noAtt, 'NO_ATTACHMENT has NO attachment') 241 | }) 242 | } 243 | 244 | function testFreshdeskTicket() { 245 | 246 | test('Ticket', function (t) { 247 | var TICKET_ID = 1 248 | // ??? var EXPECTED_ID = 9000658396 // Agent ID of Mike@zixia.net 249 | 250 | var MyFreshdesk = new GasFreshdesk(FRESHDESK_URL, FRESHDESK_KEY) 251 | var oldTicket = new MyFreshdesk.Ticket(TICKET_ID) 252 | 253 | 254 | t.ok(oldTicket, 'loaded') 255 | // Logger.log(JSON.stringify(oldTicket.getRawObj())) 256 | t.equal(oldTicket.getRawObj().id, TICKET_ID, 'match ticket id') 257 | 258 | var EXAMPLE_TICKET = { 259 | 'description':'A totally rad description of a what the problem is' 260 | , 'subject':'Something like "Cannot log in"' 261 | , 'email': 'you@example.com' 262 | } 263 | var newTicket = new MyFreshdesk.Ticket(EXAMPLE_TICKET) 264 | t.ok(newTicket, 'newTicket created') 265 | t.ok(newTicket.getId(), 'newTicket id exist') 266 | 267 | newTicket.close() 268 | // Logger.log(JSON.stringify((newTicket.getRawObj()))) 269 | newTicket.open() 270 | 271 | var ZIXIA_RESPONDER_ID = 9005923152 // zixia@zixia.net 272 | var MIKE_RESPONDER_ID = 9005923143 // mike@zixia.net 273 | 274 | t.equal(newTicket.getResponderId(), null, 'new ticket default no responder') 275 | newTicket.assign(MIKE_RESPONDER_ID) 276 | t.equal(newTicket.getResponderId(), MIKE_RESPONDER_ID, 'assigned to mike') 277 | 278 | 279 | // var numNotes = newTicket.getRawObj().notes ? newTicket.getRawObj().notes.length : 0 280 | var numNotes = newTicket.getRawObj().conversations ? newTicket.getRawObj().conversations.length : 0 281 | 282 | newTicket.note({ 283 | body: 'Hi tom, Still Angry' 284 | , private: true 285 | , attachments: [ 286 | Utilities.newBlob('TEST DATA').setName('test-data.dat') 287 | , Utilities.newBlob('TEST DATA2').setName('test-data2.dat') 288 | ] 289 | }) 290 | 291 | newTicket.note({ 292 | body: 'Hi tom, Still Angry' 293 | , private: true 294 | }) 295 | 296 | // reply also in notes[] 297 | newTicket.reply({ 298 | body: 'Replied: Hi tom, Still Angry' 299 | // , user_id: MIKE_RESPONDER_ID 300 | // , cc_emails: 301 | }) 302 | 303 | // Logger.log(JSON.stringify(newTicket.getRawObj())) 304 | 305 | // var newNumNotes = newTicket.getRawObj().notes ? newTicket.getRawObj().notes.length : 0 306 | var newNumNotes = newTicket.getRawObj().conversations ? newTicket.getRawObj().conversations.length : 0 307 | 308 | t.equal(newNumNotes, numNotes+3, 'new note(2) and reply(1) created') 309 | 310 | 311 | 312 | 313 | var priority = newTicket.getPriority() 314 | newTicket.setPriority(priority+1) 315 | t.equal(newTicket.getPriority(), priority+1, 'inc priority by 1') 316 | 317 | var status = newTicket.getStatus() 318 | newTicket.setStatus(status+1) 319 | t.equal(newTicket.getStatus(), status+1, 'inc status by 1') 320 | 321 | t.ok(newTicket.del(), 'delete newTicket') 322 | 323 | t.ok(newTicket.restore(), 'restore newTicket') 324 | 325 | t.ok(newTicket.del(), 'delete newTicket again') 326 | 327 | var EXAMPLE_TICKET_WITH_ATTACHMENTS = { 328 | 'description':'A totally rad description of a what the problem is' 329 | , 'subject':'Something like "Cannot log in"' 330 | , 'email': 'you@example.com' 331 | , attachments: [ Utilities.newBlob('TEST DATA').setName('test-data.dat') 332 | , Utilities.newBlob('TEST DATA2').setName('test-data2.dat') 333 | ] 334 | } 335 | var newTicketWithAttachment = new MyFreshdesk.Ticket(EXAMPLE_TICKET_WITH_ATTACHMENTS) 336 | t.ok(newTicketWithAttachment, 'newTicketWithAttachment created') 337 | t.ok(newTicketWithAttachment.getId(), 'newTicketWithAttachment id exist') 338 | t.ok(newTicketWithAttachment.del(), 'delete newTicketWithAttachment') 339 | 340 | }) 341 | 342 | } 343 | 344 | function testFreshdeskAuth() { 345 | test('Auth Fail', function (t) { 346 | var ERR_URL = 'https://zixia.freshdesk.com' 347 | t.throws(function () { 348 | new GasFreshdesk(ERR_URL, FRESHDESK_KEY) 349 | }, 'Auth with ERR_URL') 350 | 351 | t.throws(function () { 352 | new GasFreshdesk(FRESHDESK_URL, 'error_key') 353 | }, 'Auth with error_key') 354 | 355 | t.throws(function () { 356 | new GasFreshdesk('not_exist_url', FRESHDESK_KEY) 357 | }, 'Auth with not_exist_url') 358 | 359 | t.notThrow(function () { 360 | new GasFreshdesk(FRESHDESK_URL, FRESHDESK_KEY) 361 | }, 'Auth with right setting') 362 | }) 363 | } 364 | 365 | function testHttpBackend() { 366 | test('Multipart body process', function (t) { 367 | 368 | var http = new GasFreshdesk.Http(FRESHDESK_URL, FRESHDESK_KEY) 369 | 370 | var BLOB1 = Utilities.newBlob('XXX').setName('xxx') 371 | var BLOB2 = Utilities.newBlob('TODO').setName('todo') 372 | var OBJ = { 373 | attachments: [ 374 | BLOB1 375 | , BLOB2 376 | ] 377 | , email: 'example@example.com' 378 | , subject: 'Ticket Title' 379 | , description: 'this is a sample ticket' 380 | } 381 | 382 | var EXPECTED_MULTIPART_ARRAY = [ 383 | ['attachments[]', BLOB1] 384 | , ['attachments[]', BLOB2] 385 | , ['email', 'example@example.com'] 386 | , ['subject', 'Ticket Title'] 387 | , ['description', 'this is a sample ticket'] 388 | ] 389 | 390 | /** 391 | * makeMultipartArray 392 | */ 393 | var multipartArray = http.makeMultipartArray(OBJ) 394 | t.deepEqual(multipartArray, EXPECTED_MULTIPART_ARRAY, 'makeMultipartArray') 395 | 396 | //////////////////////////////////////////////////////////////////////////////////////////////////// 397 | var EXPECTED_MULTIPART_BODY = 398 | '----boundary-seprator\r\n' 399 | + 'Content-Disposition: form-data; name="attachments[]"; filename="xxx"\r\n' 400 | + 'Content-Type: text/plain\r\n\r\nXXX\r\n' 401 | + '----boundary-seprator\r\n' 402 | + 'Content-Disposition: form-data; name="attachments[]"; filename="todo"\r\n' 403 | + 'Content-Type: text/plain\r\n\r\nTODO\r\n' 404 | + '----boundary-seprator\r\nContent-Disposition: form-data; name="email"\r\n\r\n' 405 | + 'example@example.com\r\n----boundary-seprator\r\n' 406 | + 'Content-Disposition: form-data; name="subject"\r\n\r\n' 407 | + 'Ticket Title\r\n----boundary-seprator\r\n' 408 | + 'Content-Disposition: form-data; name="description"\r\n\r\n' 409 | + 'this is a sample ticket\r\n----boundary-seprator--\r\n' 410 | 411 | EXPECTED_MULTIPART_BODY = Utilities.newBlob(EXPECTED_MULTIPART_BODY).getBytes() 412 | /** 413 | * makeMultipartBody 414 | */ 415 | var multipartBody = http.makeMultipartBody(multipartArray, '--boundary-seprator') 416 | t.deepEqual(multipartBody, EXPECTED_MULTIPART_BODY, 'makeMultipartBody') 417 | }) 418 | 419 | test('Http Methods', function (t) { 420 | var http = new GasFreshdesk.Http(FRESHDESK_URL, FRESHDESK_KEY) 421 | 422 | var data = http.get('http://httpbin.org/get?test=ok') 423 | t.equal(typeof data, 'object', 'json') 424 | if (data && data.args) var tmp = data.args.test || '' 425 | t.equal(tmp, 'ok', 'http.get') 426 | 427 | data = http.put('http://httpbin.org/put', 'test=ok') 428 | t.equal(typeof data, 'object', 'x-www-form-urlencoded') 429 | if (data && data.form) var tmp = data.form.test || '' 430 | t.equal(tmp, 'ok', 'http.put') 431 | 432 | data = http.del('http://httpbin.org/delete') 433 | t.equal(data.url, 'http://httpbin.org/delete', 'http.del') 434 | 435 | data = http.post('http://httpbin.org/post', 'test=ok') 436 | t.equal(typeof data, 'object', 'x-www-form-urlencoded') 437 | if (data && data.form) var tmp = data.form.test || '' 438 | t.equal(tmp, 'ok', 'http.post') 439 | }) 440 | } 441 | 442 | } -------------------------------------------------------------------------------- /src/gas-freshdesk-lib.js: -------------------------------------------------------------------------------- 1 | var GasFreshdesk = (function () { 2 | 'use strict' 3 | /** 4 | * 5 | * GasFreshdesk - Freshdesk API Class for Google Apps Script 6 | * 7 | * GasFreshdesk is a easy to use Freshdesk API Class for GAS(Google Apps Script) 8 | * It provides a OO(Object-Oriented) way to use Freshdesk Ticket / Contacts, etc. 9 | * 10 | * Github - https://github.com/zixia/gas-freshdesk 11 | * 12 | * Example: 13 | ```javascript 14 | var MyFreshdesk = new GasFreshdesk('https://mikebo.freshdesk.com', 'Jrg0FQNzX3tzuHbiFjYQ') 15 | 16 | var ticket = new MyFreshdesk.Ticket({ 17 | description:'A description' 18 | , subject: 'A subject' 19 | , email: 'you@example.com' 20 | , attachments: [ Utilities.newBlob('TEST DATA').setName('test-data.dat') ] 21 | }) 22 | 23 | ticket.assign(9000658396) 24 | ticket.addNote({ 25 | body: 'Hi tom, Still Angry' 26 | , private: true 27 | }) 28 | ticket.setPriority(2) 29 | ticket.setStatus(2) 30 | 31 | ticket.del() 32 | ticket.restore() 33 | ``` 34 | */ 35 | 36 | 37 | /** 38 | * 39 | * Polyfill a dummy log function 40 | * in case of forget get rid of log in library(as developing/debuging)\ 41 | * 42 | */ 43 | try { 44 | 'use strict' 45 | var throwExceptionIfRightVariableNotExist = log; 46 | } catch (e) { // not exist 47 | // Logger.log('Polyfill log: evaled in gas-freshdesk-lib') 48 | eval('var log = function () {}') 49 | } 50 | 51 | var Freshdesk = function (url, key) { 52 | 53 | if (!key || !url) throw Error('options error: key or url not exist!') 54 | 55 | var http = new Http(url, key) 56 | 57 | 58 | /** 59 | * validateAuth: try to listTickets 60 | * if url & key is not right 61 | * exception will be thrown 62 | */ 63 | validateAuth() 64 | 65 | 66 | this.http = http 67 | 68 | this.Ticket = freshdeskTicket 69 | this.Contact = freshdeskContact 70 | this.Agent = freshdeskAgent 71 | 72 | this.listTickets = freshdeskListTickets 73 | this.Ticket.list = freshdeskListTickets 74 | 75 | this.listContacts = freshdeskListContacts 76 | this.Contact.list = freshdeskListContacts 77 | 78 | this.listAgents = freshdeskListAgents 79 | this.Agent.list = freshdeskListAgents 80 | 81 | return this 82 | 83 | 84 | /********************************************************************** 85 | * 86 | * Freshdesk Instance Methods Implementation 87 | * 88 | */ 89 | 90 | 91 | /** 92 | * 93 | * make a http call to api, in order to confirm the auth token is right. 94 | * @tested 95 | */ 96 | function validateAuth() { 97 | // v1: return http.get('/helpdesk/tickets/filter/all_tickets?format=json') 98 | return http.get('/api/v2/tickets?per_page=1') 99 | } 100 | 101 | /** 102 | * 103 | * 1. Method Search Ticket 104 | * 105 | * @return {Array} Tickets of search. null for not found 106 | * 107 | * @param {Object} options 108 | * email: email address of requester 109 | * 110 | * @document https://development.freshdesk.com/api#view_all_ticket 111 | * 112 | */ 113 | function freshdeskListTickets(options) { 114 | 115 | var data 116 | 117 | if (options && options.email) { // Requester email 118 | var email = validateEmail(options.email) 119 | data = http.get('/api/v2/tickets?order_by=created_at&order_type=asc&email=' + encodeURIComponent(email)) 120 | } else if (options && options.requester_id) { 121 | var requesterId = validateInteger(options.requester_id) 122 | data = http.get('/api/v2/tickets?order_by=created_at&order_type=asc&requester_id=' + requesterId) 123 | }else { // Uses the new_and_my_open filter. 124 | data = http.get('/api/v2/tickets') 125 | 126 | } 127 | 128 | if (!data || !data.length) return [] 129 | 130 | var tickets = data.map(function (d) { 131 | return new freshdeskTicket(d.id) 132 | }) 133 | 134 | return tickets 135 | } 136 | 137 | 138 | /** 139 | * 140 | * 2. Method Search Contact 141 | * 142 | */ 143 | function freshdeskListContacts(options) { 144 | 145 | var email = options.email 146 | 147 | var data = http.get('/api/v2/contacts?email=' + encodeURIComponent(email)) 148 | 149 | if (!data || !data.length) return [] 150 | 151 | var contacts = data.map(function (d) { 152 | return new freshdeskContact(d.id) 153 | }) 154 | 155 | return contacts 156 | } 157 | 158 | /** 159 | * 160 | * 3. Method Search Agent 161 | * 162 | * @param 163 | * options.email email of agent 164 | * 165 | * @return 166 | * of , or null for not found. 167 | * 168 | */ 169 | function freshdeskListAgents(options) { 170 | 171 | var email = options.email 172 | 173 | var data = http.get('/api/v2/agents?email=' + encodeURIComponent(email)) 174 | 175 | if (!data || !data.length) return [] 176 | 177 | var agents = data.map(function (d) { 178 | return new freshdeskAgent(d.id) 179 | }) 180 | 181 | return agents 182 | } 183 | 184 | /****************************************************************** 185 | * 186 | * Class Ticket 187 | * ------------ 188 | */ 189 | function freshdeskTicket (options) { 190 | 191 | if ((typeof this) === 'undefined') return new freshdeskTicket(options) 192 | 193 | var ticketObj = {} 194 | 195 | if ((typeof options) === 'number') { 196 | 197 | /** 198 | * 1. existing ticket, retried it by ID 199 | */ 200 | 201 | var id = options 202 | 203 | reloadTicket(id) 204 | 205 | } else if ((typeof options) === 'object') { // { x: y } options 206 | 207 | /** 208 | * 2. new ticket. create it. 209 | */ 210 | 211 | if (!options.status) options.status = 2 // Status.Open 212 | if (!options.priority) options.priority = 1 // Priority.Low 213 | 214 | validateHelpdeskObject(options) 215 | // v1 ticketObj = http.post('/helpdesk/tickets.json', options) 216 | ticketObj = http.post('/api/v2/tickets', options) 217 | 218 | } else { 219 | // 3. error. 220 | throw Error('options must be integer or object') 221 | } 222 | 223 | this.getId = getTicketId 224 | this.getResponderId = getResponderId 225 | this.getRequesterId = getRequesterId 226 | this.assign = assignTicket 227 | this.note = noteTicket 228 | this.reply = replyTicket 229 | 230 | this.del = deleteTicket 231 | this.restore = restoreTicket 232 | 233 | this.getPriority = getTicketPriority 234 | this.setPriority = setTicketPriority 235 | 236 | this.getStatus = getTicketStatus 237 | this.setStatus = setTicketStatus 238 | 239 | this.getGroup = getTicketGroup 240 | this.setGroup = setTicketGroup 241 | 242 | this.open = function () { return setTicketStatus(2) } 243 | this.pend = function () { return setTicketStatus(3) } 244 | this.resolv = function () { return setTicketStatus(4) } 245 | this.close = function () { return setTicketStatus(5) } 246 | 247 | this.lowPriority = function () { return setTicketPriority(1) } 248 | this.mediumPriority = function () { return setTicketPriority(2) } 249 | this.highPriority = function () { return setTicketPriority(3) } 250 | 251 | this.getRawObj = function () { return ticketObj } 252 | 253 | // this.setCustomField = setTicketCustomField 254 | // this.setTag = setTicketTag 255 | 256 | 257 | return this 258 | 259 | /////////////////////////////////////////////////////////// 260 | 261 | function getTicketId() { 262 | // Logger.log(JSON.stringify(ticketObj)) 263 | if (ticketObj && ticketObj.id) { 264 | return ticketObj.id 265 | } 266 | 267 | return null 268 | } 269 | 270 | function getResponderId() { 271 | 272 | if (ticketObj && ticketObj.responder_id) { 273 | return ticketObj.responder_id 274 | } 275 | 276 | return null 277 | } 278 | 279 | function getRequesterId() { 280 | 281 | if (ticketObj && ticketObj.requester_id) { 282 | return ticketObj.requester_id 283 | } 284 | 285 | return null 286 | } 287 | 288 | function assignTicket(responderId) { 289 | 290 | // v1: 291 | // http.put('/helpdesk/tickets/' 292 | // + getTicketId() 293 | // + '/assign.json?responder_id=' 294 | // + responderId 295 | // ) 296 | 297 | http.put('/api/v2/tickets/' + getTicketId(), { 298 | responder_id: responderId 299 | }) 300 | 301 | reloadTicket(getTicketId()) // refresh 302 | 303 | return this 304 | } 305 | 306 | function deleteTicket() { 307 | // v1: if ('deleted'==http.del('/helpdesk/tickets/' + getTicketId() + '.json')) { 308 | http.del('/api/v2/tickets/' + getTicketId()) 309 | reloadTicket(getTicketId()) // refresh 310 | return true 311 | } 312 | 313 | /** 314 | * 315 | * @tested 316 | */ 317 | function restoreTicket(id) { 318 | 319 | if (!id) id = getTicketId() 320 | 321 | if (id%1 !== 0) throw Error('ticket id(' + id + ') must be integer') 322 | 323 | // v1: var ret = http.put('/helpdesk/tickets/' + id + '/restore.json') 324 | var ret = http.put('/api/v2/tickets/' + id + '/restore') 325 | 326 | reloadTicket(getTicketId()) // refresh 327 | return this 328 | } 329 | 330 | /** 331 | * 332 | * Reload Ticket Object Raw Data 333 | * 334 | */ 335 | function reloadTicket(id) { 336 | 337 | if (id%1 !== 0) throw Error('ticket id(' + id + ') must be integer.') 338 | // Logger.log('loading id:' + id) 339 | // v1: ticketObj = http.get('/helpdesk/tickets/' + id + '.json') 340 | // ticketObj = http.get('/api/v2/tickets/' + id + '?include=notes') 341 | ticketObj = http.get('/api/v2/tickets/' + id + '?include=conversations') 342 | // Logger.log(JSON.stringify(ticketObj)) 343 | return this 344 | } 345 | 346 | /** 347 | * 348 | * Note a Ticket 349 | * 350 | * @tested 351 | */ 352 | function noteTicket(data) { 353 | 354 | validateHelpdeskObject(data) 355 | 356 | // v1: var retVal = http.post('/helpdesk/tickets/' + getTicketId() + '/conversations/note.json', data) 357 | var retVal = http.post('/api/v2/tickets/' + getTicketId() + '/notes', data) 358 | 359 | if (retVal) { 360 | reloadTicket(getTicketId()) 361 | return true 362 | } 363 | 364 | return false 365 | } 366 | 367 | /** 368 | * 369 | * Reply a Ticket 370 | * 371 | * @testing 372 | */ 373 | function replyTicket(data) { 374 | 375 | validateHelpdeskObject(data) 376 | 377 | var retVal = http.post('/api/v2/tickets/' + getTicketId() + '/reply', data) 378 | 379 | if (retVal) { 380 | reloadTicket(getTicketId()) 381 | return true 382 | } 383 | 384 | return false 385 | } 386 | 387 | /** 388 | * 389 | * 390 | * @tested 391 | */ 392 | function getTicketPriority() { return ticketObj.priority } 393 | function setTicketPriority(priority) { 394 | // v1: var retVal = http.put('/helpdesk/tickets/' + getTicketId() + '.json', { 395 | var retVal = http.put('/api/v2/tickets/' + getTicketId(), { 396 | priority: priority 397 | }) 398 | 399 | if (retVal) { 400 | reloadTicket(getTicketId()) 401 | return this 402 | } 403 | 404 | throw Error('set priority fail') 405 | } 406 | 407 | /** 408 | * 409 | * 410 | * @tested 411 | */ 412 | function getTicketStatus() { return ticketObj.status } 413 | function setTicketStatus(status) { 414 | // v1: var retVal = http.put('/helpdesk/tickets/' + getTicketId() + '.json', { 415 | var retVal = http.put('/api/v2/tickets/' + getTicketId(), { 416 | status: status 417 | }) 418 | 419 | if (retVal) { 420 | reloadTicket(getTicketId()) 421 | return this 422 | } 423 | 424 | throw Error('set status fail') 425 | } 426 | 427 | function getTicketGroup() { return ticketObj.group_id } 428 | function setTicketGroup(groupId) { 429 | var retVal = http.put('/api/v2/tickets/' + getTicketId(), { 430 | group_id: groupId 431 | }) 432 | 433 | if (retVal) { 434 | reloadTicket(getTicketId()) 435 | return this 436 | } 437 | 438 | throw Error('set group fail') 439 | } 440 | 441 | function setTicketCustomField(customFields) { 442 | // v1: var retVal = http.put('/helpdesk/tickets/' + getTicketId() + '.json', { 443 | var retVal = http.put('/api/v2/tickets/' + getTicketId(), { 444 | custom_field: customFields 445 | }) 446 | 447 | if (retVal) { 448 | reloadTicket(getTicketId()) 449 | return this 450 | } 451 | 452 | throw Error('set status fail') 453 | } 454 | 455 | function setTicketTag(tags) { 456 | 457 | throw Error('not implenment yet') 458 | 459 | var ticketTags = ticketObj.tags 460 | 461 | // "tags":[ 462 | // {"name": "tag1"}, 463 | // {"name": "tag2"} 464 | // ] 465 | 466 | // v1: var retVal = http.put('/helpdesk/tickets/' + getTicketId() + '.json', { 467 | var retVal = http.put('/api/v2/tickets/' + getTicketId(), { 468 | helpdesk: { 469 | tags: ticketTags 470 | } 471 | }) 472 | 473 | if (retVal) { 474 | reloadTicket(getTicketId()) 475 | return this 476 | } 477 | 478 | throw Error('set tags fail') 479 | } 480 | 481 | //////////////////////////////// 482 | }// Seprator of Ticket Instance 483 | //////////////////////////////// 484 | 485 | /*************************************************************************** 486 | * 487 | * Class Contact 488 | * ------------- 489 | */ 490 | function freshdeskContact(options) { 491 | 492 | if ((typeof this) === 'undefined') return new freshdeskContact(options) 493 | 494 | var contactObj = {} 495 | 496 | if ((typeof options) === 'number') { 497 | 498 | /** 499 | * 1. existing contact, get it by ID 500 | */ 501 | 502 | id = options 503 | 504 | reloadContact(id) 505 | } else if ((typeof options) === 'object') { // { x: y } options 506 | 507 | /** 508 | * 2. new contact. create it. 509 | */ 510 | 511 | // v1: contactObj = http.post('/contacts.json', options) 512 | contactObj = http.post('/api/v2/contacts', options) 513 | 514 | } else { 515 | // 3. error. 516 | throw Error('options must be integer or options') 517 | } 518 | 519 | this.getId = getContactId 520 | 521 | this.del = deleteContact 522 | 523 | this.getName = getContactName 524 | this.setName = setContactName 525 | 526 | this.getEmail = getContactEmail 527 | 528 | this.getTitle = getContactTitle 529 | this.setTitle = setContactTitle 530 | 531 | this.getRawObj = function () { return contactObj } 532 | 533 | 534 | return this 535 | 536 | //////////////////////////////////////////////////////// 537 | 538 | function getContactId() { 539 | if (contactObj && contactObj.id) { 540 | return contactObj.id 541 | } 542 | 543 | return null 544 | } 545 | 546 | function deleteContact() { 547 | // v1: if ('deleted'==http.del('/contacts/' + getContactId() + '.json')) { 548 | if ('deleted'==http.del('/api/v2/contacts/' + getContactId())) { 549 | reloadContact(getContactId()) // refresh 550 | return true 551 | } 552 | return false 553 | } 554 | 555 | /** 556 | * 557 | * Reload Contact Object Raw Data 558 | * 559 | */ 560 | function reloadContact(id) { 561 | 562 | if ((typeof id)=='undefined') id = getContactId() 563 | 564 | if (id%1 !== 0) throw Error('contact id(' + id + ') must be integer.') 565 | 566 | // v1: contactObj = http.get('/contacts/' + id + '.json') 567 | contactObj = http.get('/api/v2/contacts/' + id) 568 | 569 | return this 570 | } 571 | 572 | /** 573 | * 574 | * 575 | * @testing 576 | */ 577 | function getContactName() { 578 | return contactObj.name 579 | } 580 | function setContactName(name) { 581 | // v1: var retVal = http.put('/contacts/' + getContactId() + '.json', { 582 | var retVal = http.put('/api/v2/contacts/' + getContactId(), { 583 | name: name 584 | }) 585 | 586 | if (retVal) { 587 | reloadContact() 588 | return this 589 | } 590 | 591 | throw Error('set name fail') 592 | } 593 | 594 | function getContactEmail() { 595 | return contactObj.email 596 | } 597 | 598 | /** 599 | * 600 | * 601 | * @testing 602 | */ 603 | function getContactTitle() { return contactObj.job_title } 604 | function setContactTitle(title) { 605 | // v1: var retVal = http.put('/contacts/' + getTicketId() + '.json', { 606 | var retVal = http.put('/api/v2/contacts/' + getTicketId(), { 607 | user: { 608 | job_title: title 609 | } 610 | }) 611 | 612 | if (retVal) { 613 | reloadContact() 614 | return this 615 | } 616 | 617 | throw Error('set status fail') 618 | } 619 | 620 | 621 | //////////////////////////////// 622 | }// Seprator of Contact Instance 623 | //////////////////////////////// 624 | 625 | 626 | /*************************************************************************** 627 | * 628 | * Class Agent 629 | * ----------- 630 | */ 631 | function freshdeskAgent(id) { 632 | 633 | if ((typeof this) === 'undefined') return new freshdeskAgent(options) 634 | 635 | var agentObj = {} 636 | 637 | if ((typeof id) === 'number') { 638 | 639 | /** 640 | * 1. existing agent, get it by ID 641 | */ 642 | 643 | // load #id to agentObj 644 | reloadAgent(id) 645 | 646 | } else { 647 | // 2. error. 648 | throw Error('id must be integer') 649 | } 650 | 651 | this.getId = getAgentId 652 | this.getName = getAgentName 653 | 654 | this.getRawObj = function () { return agentObj } 655 | 656 | 657 | return this 658 | 659 | /////////////////////////////////////////////// 660 | 661 | function getAgentId() { 662 | if (agentObj && agentObj.id) { 663 | return agentObj.id 664 | } 665 | 666 | return null 667 | } 668 | 669 | function getAgentName() { 670 | return agentObj.contact.name 671 | } 672 | 673 | /** 674 | * 675 | * Reload Agent Object Raw Data 676 | * 677 | */ 678 | function reloadAgent(id) { 679 | 680 | if ((typeof id)=='undefined') id = getAgentId() 681 | 682 | if (id%1 !== 0) throw Error('agent id(' + id + ') must be integer.') 683 | 684 | // v1: agentObj = http.get('/agents/' + id + '.json') 685 | agentObj = http.get('/api/v2/agents/' + id) 686 | 687 | return this 688 | } 689 | 690 | //////////////////////////////// 691 | }// Seprator of Agent Instance 692 | //////////////////////////////// 693 | 694 | } 695 | 696 | 697 | // export for testing only 698 | Freshdesk.Http = Http 699 | Freshdesk.validateHelpdeskObject = validateHelpdeskObject 700 | Freshdesk.validEmail = validateEmail 701 | Freshdesk.validateInteger = validateInteger 702 | 703 | return Freshdesk 704 | 705 | /////////////////////////////////////////////////////////////////////////////////////// 706 | // 707 | // Class Static Methods Implementation 708 | // 709 | /////////////////////////////////////////////////////////////////////////////////////// 710 | 711 | 712 | /*********************************************************************** 713 | * 714 | * Class Http 715 | * ---------- 716 | * Backend Class for Freshdesk Rest API 717 | * 718 | * options.key 719 | * options.type 720 | * 721 | */ 722 | function Http(url, key) { 723 | 724 | if (!url || !key) throw Error('url & key must set!') 725 | 726 | var URL = url 727 | var KEY = key 728 | var AUTH_HEADER = { 729 | 'Authorization': 'Basic ' + Utilities.base64Encode(KEY + ':X') 730 | } 731 | 732 | return { 733 | get: get 734 | , put: put 735 | , post: post 736 | , del: del 737 | 738 | , httpBackend: httpBackend 739 | 740 | , makeMultipartArray: makeMultipartArray 741 | , makeMultipartBody: makeMultipartBody 742 | 743 | , hasAttachment: hasAttachment 744 | 745 | } 746 | 747 | function get(path) { 748 | return httpBackend('get', path) 749 | } 750 | 751 | function put(path, data) { 752 | return httpBackend('put', path, data) 753 | } 754 | 755 | function post(path, data) { 756 | return httpBackend('post', path, data) 757 | } 758 | 759 | function del(path) { 760 | return httpBackend('delete', path) 761 | } 762 | 763 | /** 764 | * 765 | * HTTP Backend Engine 766 | * 767 | */ 768 | function httpBackend(method, path, data) { 769 | 770 | var contentType, payload 771 | 772 | if (method=='post' && hasAttachment(data)) { 773 | 774 | var BOUNDARY = '-----CUTHEREelH7faHNSXWNi72OTh08zH29D28Zhr3Rif3oupOaDrj' 775 | var multipartArray = makeMultipartArray(data) 776 | 777 | // log(JSON.stringify(multipartArray)) 778 | 779 | contentType = 'multipart/form-data; boundary=' + BOUNDARY 780 | payload = makeMultipartBody(multipartArray, BOUNDARY) 781 | 782 | } else if (!data || data instanceof Object) { 783 | 784 | /** 785 | * 786 | * When we pass a object as payload to UrlFetchApp.fetch, it will treat object as a key=value form-urlencoded type. 787 | * 788 | * If we want to post JSON object via fetch, we must: 789 | * 1. specify contentType to 'application/json' 790 | * 2. payload should already be JSON.stringify(ed) 791 | * 792 | */ 793 | contentType = 'application/json' 794 | payload = JSON.stringify(data) 795 | 796 | } else { 797 | 798 | contentType = 'application/x-www-form-urlencoded' 799 | payload = data 800 | 801 | } 802 | 803 | var options = { 804 | muteHttpExceptions: true 805 | , headers: AUTH_HEADER 806 | , method: method 807 | } 808 | 809 | switch (method.toLowerCase()) { 810 | case 'post': 811 | case 'put': 812 | options.contentType = contentType 813 | options.payload = payload 814 | break 815 | 816 | default: 817 | case 'get': 818 | case 'delete': 819 | break 820 | 821 | } 822 | 823 | 824 | if (/^http/.test(path)) { 825 | var endpoint = path 826 | } else { 827 | endpoint = URL + path 828 | } 829 | 830 | /** 831 | * 832 | * UrlFetch fetch API EndPoint 833 | * 834 | */ 835 | 836 | var TTL = 3 837 | var response = undefined 838 | var retCode = undefined 839 | 840 | while (!retCode && TTL--) { 841 | try { 842 | response = UrlFetchApp.fetch(endpoint, options) 843 | retCode = response.getResponseCode() 844 | } catch (e) { 845 | log(log.DEBUG, 'UrlFetchApp.fetch exception(ttl:%s): %s, %s', TTL, e.name, e.message) 846 | Utilities.sleep(50) // sleep 50 ms 847 | } 848 | // Logger.log('ttl:' + TTL + ', retCode:' + retCode) 849 | } 850 | 851 | switch (true) { 852 | case /^2/.test(retCode): 853 | // It's ok with 2XX 854 | break; 855 | 856 | case /^3/.test(retCode): 857 | // TBD: OK? NOT OK??? 858 | break; 859 | 860 | case /^4/.test(retCode): 861 | case /^5/.test(retCode): 862 | /** 863 | * 864 | * Get Detail Error Response from Freshdesk API v2 865 | * http://developer.freshdesk.com/api/#error 866 | * 867 | */ 868 | var apiErrorMsg 869 | 870 | try { 871 | var respObj = JSON.parse(response.getContentText()); 872 | 873 | var description = respObj.description 874 | var errors = respObj.errors 875 | 876 | var errorMsg 877 | 878 | if (errors && errors instanceof Array) { 879 | errorMsg = errors.map(function (e) { 880 | return Utilities.formatString('code[%s], field[%s], message[%s]' 881 | , e.code || '' 882 | , e.field || '' 883 | , e.message || '' 884 | ) 885 | }).reduce(function (v1, v2) { 886 | return v1 + '; ' + v2 887 | }); 888 | 889 | } else if (respObj.code) { 890 | errorMsg = Utilities.formatString('code[%s], field[%s], message[%s]' 891 | , respObj.code || '' 892 | , respObj.field || '' 893 | , respObj.message || '' 894 | ) 895 | } 896 | 897 | // clean options 898 | if (options.payload) { 899 | options.payload = options.payload ? JSON.parse(options.payload) : {} 900 | 901 | if (options.payload.body) options.payload.body = '...STRIPED...' 902 | if (options.payload.description) options.payload.description = '...STRIPED...' 903 | } 904 | options = JSON.stringify(options) 905 | 906 | apiErrorMsg = Utilities 907 | .formatString('Freshdesk API v2 failed when calling endpoint[%s], options[%s], description[%s] with error: (%s)' 908 | , endpoint 909 | , options 910 | , description || '' 911 | , errorMsg || '' 912 | ) 913 | } catch (e) { 914 | Logger.log(e.name + ',' + e.message + ',' + e.stack) 915 | } 916 | 917 | if (apiErrorMsg) 918 | throw Error(apiErrorMsg); 919 | 920 | throw Error('http call failed with http code:' + response.getResponseCode()); 921 | 922 | break; 923 | 924 | default: 925 | var errMsg = [ 926 | 'endpoint: ' + endpoint 927 | , 'options: ' + JSON.stringify(options) 928 | , (response ? response.getContentText().substring(0,1000) : '(undefined)') 929 | , 'api call failed with http code:' + (response ? response.getResponseCode() : '(undefined)') 930 | ].join(', ') 931 | 932 | throw Error(errMsg) 933 | break 934 | } 935 | 936 | var retContent = response.getContentText() 937 | 938 | /** 939 | * Object in object out 940 | * String in string out 941 | */ 942 | var retObj 943 | 944 | switch (true) { 945 | case /x-www-form-urlencoded/.test(contentType): 946 | try { 947 | retObj = JSON.parse(retContent) 948 | } catch (e) { 949 | // it's ok here, just let ret be string. 950 | retObj = retContent 951 | } 952 | 953 | break; 954 | 955 | default: 956 | case /multipart/.test(contentType): 957 | case /json/.test(contentType): 958 | try { 959 | retObj = JSON.parse(retContent) 960 | } catch (e) { 961 | /** 962 | * something went wrong here! 963 | * because we need: Object in object out 964 | */ 965 | retObj = { 966 | error: e.message 967 | , string: retContent 968 | } 969 | } 970 | break 971 | } 972 | 973 | // Freshdesk API will set `require_login` if login failed 974 | if (retObj && retObj.require_login) throw Error('auth failed to url ' + URL + ' with key ' + KEY) 975 | 976 | return retObj 977 | 978 | } 979 | 980 | /** 981 | * 982 | * @param object data 983 | * @return string a multipart body 984 | * 985 | * concat attachments for array [attachments][] 986 | * 987 | * @testing 988 | */ 989 | function makeMultipartBody(multipartArray, boundary) { 990 | 991 | var body = Utilities.newBlob('').getBytes() 992 | 993 | for (var i in multipartArray) { 994 | var [k, v] = multipartArray[i] 995 | 996 | // log('multipartArray[' + k + ']') 997 | 998 | if (v.toString() == 'Blob' 999 | || v.toString() == 'GmailAttachment' 1000 | ) { 1001 | 1002 | // log(v.toString()) 1003 | // log(v) 1004 | // log(typeof v) 1005 | 1006 | // Object.keys(v).forEach(function (k) { 1007 | // log('v[' + k + ']') 1008 | // }) 1009 | 1010 | // attachment 1011 | body = body.concat( 1012 | Utilities.newBlob( 1013 | '--' + boundary + '\r\n' 1014 | + 'Content-Disposition: form-data; name="' + k + '"; filename="' + v.getName() + '"\r\n' 1015 | + 'Content-Type: ' + v.getContentType() + '\r\n\r\n' 1016 | ).getBytes()) 1017 | 1018 | body = body 1019 | .concat(v.getBytes()) 1020 | .concat(Utilities.newBlob('\r\n').getBytes()) 1021 | 1022 | } else { 1023 | 1024 | // string 1025 | body = body.concat( 1026 | Utilities.newBlob( 1027 | '--'+boundary+'\r\n' 1028 | + 'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' 1029 | + v + '\r\n' 1030 | ).getBytes() 1031 | ) 1032 | 1033 | } 1034 | 1035 | } 1036 | 1037 | body = body.concat(Utilities.newBlob('--' + boundary + "--\r\n").getBytes()) 1038 | 1039 | return body 1040 | 1041 | } 1042 | 1043 | /** 1044 | * 1045 | * @param object obj 1046 | * 1047 | * @return Array [ [k,v], ... ] 1048 | * @tested 1049 | */ 1050 | function makeMultipartArray(obj) { 1051 | 1052 | var multipartArray = new Array() 1053 | 1054 | for (var k in obj) { 1055 | recursion(k, obj[k]) 1056 | } 1057 | 1058 | return multipartArray 1059 | 1060 | 1061 | function recursion(key, value) { 1062 | if ((typeof value)=='object' && !isAttachment(value)) { 1063 | for (var k in value) { 1064 | if (value instanceof Array) { 1065 | 1066 | // recursion for Array 1067 | recursion(key + '[]', value[k]) 1068 | 1069 | } else { 1070 | 1071 | // recursion for Object 1072 | recursion(key + '[' + k + ']', value[k]) 1073 | 1074 | } 1075 | } 1076 | } else { 1077 | 1078 | // Push result to Array 1079 | multipartArray.push([key, value]) 1080 | 1081 | } 1082 | } 1083 | 1084 | } 1085 | 1086 | /** 1087 | * 1088 | * Walk through a object, return true if there has any key named "attachments" 1089 | * @tested 1090 | */ 1091 | function hasAttachment(obj) { 1092 | 1093 | if ((typeof obj) != 'object') return false 1094 | 1095 | var hasAtt = false 1096 | 1097 | var keys = Object.keys(obj) 1098 | 1099 | for (var i=0; i?$/i 1131 | 1132 | if (RE.test(email)) return email 1133 | 1134 | throw Error('invalid email: [' + email + ']') 1135 | } 1136 | 1137 | function validateInteger(num) { 1138 | if (num%1===0) return num 1139 | else throw Error('invalid integer: [' + num + ']') 1140 | } 1141 | 1142 | /** 1143 | * freshdesk api v2 has better error checking for us. 1144 | */ 1145 | function validateHelpdeskObject(obj) { 1146 | 1147 | if (!obj || (typeof obj!=='object')) throw Error('invalid helpdesk object: it is not object.') 1148 | 1149 | if (obj.email) validateEmail(obj.email) 1150 | 1151 | // unknown treat as ok 1152 | return true 1153 | } 1154 | 1155 | }()) 1156 | --------------------------------------------------------------------------------