├── Code.gs ├── LICENSE └── README.org /Code.gs: -------------------------------------------------------------------------------- 1 | /*SETTING UP: 2 | follow https://developers.facebook.com/docs/messenger-platform/quickstart but to get your webhook URL: 3 | go to the cloud icon (5th from the left) and make sure at the bottom "Who has access to the app:" = "Anyone, even anonymous", then press "DEPLOY" and use the resulting url 4 | */ 5 | //MAKE SURE EACH TIME UPDATE: GO TO THE CLOUD ICON (5th icon from the left), click "Project Version"->"New" and click the Update Button 6 | 7 | 8 | var DEBUG = true 9 | //used to debug, will add to this instead of Logger.log (can't bc is being triggered outside the normal GAS runtime) 10 | var SPREADSHEET_ID = "PUT_SPREADHSEET_ID_HERE (make new spreadsheet and is part after "/d")" 11 | var ACCESS_TOKEN = "PUT_ACCESS_TOKEN_HERE" 12 | 13 | 14 | function log(subject, body){ 15 | // MailApp.sendEmail("jonahmail1@gmail.com", subject, body) 16 | if(DEBUG){ 17 | SpreadsheetApp.openById(SPREADSHEET_ID).appendRow([subject, body]) 18 | } 19 | } 20 | 21 | function doGet(request) { 22 | //this is for renewing the webhook 23 | // https://developers.facebook.com/apps/863668370411029/messenger/ 24 | // 25 | log("gotrequest", JSON.stringify(request)) 26 | // log("gotrequest", request.parameters["hub.challenge"][0]) 27 | if(request.parameters["hub.verify_token"] == "is_password"){ 28 | log("gotVerify", "") 29 | return ContentService.createTextOutput(request.parameters["hub.challenge"][0]); 30 | } 31 | return ContentService.createTextOutput(""); 32 | } 33 | 34 | function doPost(request){ 35 | try{ 36 | log("gotrequest", request.postData.contents) 37 | // return ContentService.createTextOutput("NOT RIGHT TOKEN"); 38 | 39 | var data = JSON.parse(request.postData.contents)//.contents; 40 | log(data.object, "") 41 | 42 | // Make sure this is a page subscription 43 | if (data.object == 'page') { 44 | // Iterate over each entry 45 | // There may be multiple if batched 46 | data.entry.forEach(function(pageEntry) { 47 | var pageID = pageEntry.id; 48 | var timeOfEvent = pageEntry.time; 49 | 50 | // Iterate over each messaging event 51 | pageEntry.messaging.forEach(function(messagingEvent) { 52 | if (messagingEvent.optin) { 53 | //todo but shoukd only be if public: https://developers.facebook.com/docs/messenger-platform/plugin-reference/send-to-messenger 54 | //receivedAuthentication(messagingEvent); 55 | } else if (messagingEvent.message) { 56 | log("got message", JSON.stringify(messagingEvent)) 57 | receivedMessage(messagingEvent); 58 | } else if (messagingEvent.delivery) { 59 | log("got delivery confirmait+on", JSON.stringify(messagingEvent)) 60 | // receivedDeliveryConfirmation(messagingEvent); 61 | } else if (messagingEvent.postback) { 62 | log("got postback", JSON.stringify(messagingEvent)) 63 | receivedPostback(messagingEvent); 64 | } else { 65 | //also hook on account_linking but not doing anything 66 | log("Webhook received unknown messagingEvent: ", JSON.stringify(messagingEvent)); 67 | } 68 | }); 69 | }); 70 | return ContentService.createTextOutput("ALL GOOD"); 71 | }else{ 72 | log("dataObject not page", "") 73 | } 74 | }catch(e){ 75 | log("error", e) 76 | 77 | } 78 | } 79 | 80 | function test(){ 81 | // sendTextMessage("1359249650767661", "hellop") 82 | sendGenericMessage("1359249650767661") 83 | } 84 | 85 | 86 | function receivedMessage(event) { 87 | var senderID = event.sender.id; 88 | var recipientID = event.recipient.id; 89 | var timeOfMessage = event.timestamp; 90 | var message = event.message; 91 | 92 | // Logger.log("Received message for user %d and page %d at %d with message:", 93 | // senderID, recipientID, timeOfMessage); 94 | log("Received message for user "+senderID+" and page "+recipientID+" at "+timeOfMessage+" with message:"+message) 95 | // Logger.log(JSON.stringify(message)); 96 | 97 | var messageId = message.mid; 98 | 99 | // You may get a text or attachment but not both 100 | var messageText = message.text; 101 | var messageAttachments = message.attachments; 102 | 103 | if (messageText) { 104 | 105 | // If we receive a text message, check to see if it matches any special 106 | // keywords and send back the corresponding example. Otherwise, just echo 107 | // the text we received. 108 | switch (messageText) { 109 | case 'image': 110 | sendImageMessage(senderID); 111 | break; 112 | 113 | case 'button': 114 | sendButtonMessage(senderID); 115 | break; 116 | 117 | case 'generic': 118 | sendGenericMessage(senderID); 119 | break; 120 | 121 | case 'receipt': 122 | sendReceiptMessage(senderID); 123 | break; 124 | case 'pick': 125 | sendPick(senderID) 126 | break; 127 | case 'airport': 128 | sendAirline(senderID); 129 | break; 130 | default: 131 | sendTextMessage(senderID, messageText); 132 | } 133 | } else if (messageAttachments) { 134 | sendTextMessage(senderID, "Message with attachment received"); 135 | } 136 | } 137 | 138 | function sendGenericMessage(recipientId) { 139 | var messageData = { 140 | recipient: { 141 | id: recipientId 142 | }, 143 | message: { 144 | attachment: { 145 | type: "template", 146 | payload: { 147 | template_type: "generic", 148 | elements: [{ 149 | title: "rift", 150 | subtitle: "Next-generation virtual reality", 151 | item_url: "https://www.oculus.com/en-us/rift/", 152 | image_url: "http://messengerdemo.parseapp.com/img/rift.png", 153 | buttons: [{ 154 | type: "web_url", 155 | url: "https://www.oculus.com/en-us/rift/", 156 | title: "Open Web URL" 157 | }, { 158 | type: "postback", 159 | title: "Call Postback", 160 | payload: "Payload for first bubble", 161 | }], 162 | }, { 163 | title: "touch", 164 | subtitle: "Your Hands, Now in VR", 165 | item_url: "https://www.oculus.com/en-us/touch/", 166 | image_url: "http://messengerdemo.parseapp.com/img/touch.png", 167 | buttons: [{ 168 | type: "web_url", 169 | url: "https://www.oculus.com/en-us/touch/", 170 | title: "Open Web URL" 171 | }, { 172 | type: "postback", 173 | title: "Call Postback", 174 | payload: "Payload for second bubble", 175 | }] 176 | }] 177 | } 178 | } 179 | } 180 | }; 181 | 182 | callSendAPI(messageData); 183 | } 184 | 185 | function sendPick(recipientId) { 186 | var messageData = { 187 | recipient: { 188 | id: recipientId 189 | }, 190 | "message":{ 191 | "text":"Pick a color:", 192 | "quick_replies":[ 193 | { 194 | "content_type":"text", 195 | "title":"Red", 196 | "payload":"DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_RED" 197 | }, 198 | { 199 | "content_type":"text", 200 | "title":"Green", 201 | "payload":"DEVELOPER_DEFINED_PAYLOAD_FOR_PICKING_GREEN" 202 | } 203 | ] 204 | } 205 | }; 206 | 207 | callSendAPI(messageData); 208 | } 209 | 210 | function sendAirline(recipientId) { 211 | var messageData = { 212 | recipient: { 213 | id: recipientId 214 | }, 215 | "message": { 216 | "attachment": { 217 | "type": "template", 218 | "payload": { 219 | "template_type": "airline_itinerary", 220 | "intro_message": "Here\'s your flight itinerary.", 221 | "locale": "en_US", 222 | "pnr_number": "ABCDEF", 223 | "passenger_info": [ 224 | { 225 | "name": "Farbound Smith Jr", 226 | "ticket_number": "0741234567890", 227 | "passenger_id": "p001" 228 | }, 229 | { 230 | "name": "Nick Jones", 231 | "ticket_number": "0741234567891", 232 | "passenger_id": "p002" 233 | } 234 | ], 235 | "flight_info": [ 236 | { 237 | "connection_id": "c001", 238 | "segment_id": "s001", 239 | "flight_number": "KL9123", 240 | "aircraft_type": "Boeing 737", 241 | "departure_airport": { 242 | "airport_code": "SFO", 243 | "city": "San Francisco", 244 | "terminal": "T4", 245 | "gate": "G8" 246 | }, 247 | "arrival_airport": { 248 | "airport_code": "SLC", 249 | "city": "Salt Lake City", 250 | "terminal": "T4", 251 | "gate": "G8" 252 | }, 253 | "flight_schedule": { 254 | "departure_time": "2016-01-02T19:45", 255 | "arrival_time": "2016-01-02T21:20" 256 | }, 257 | "travel_class": "business" 258 | }, 259 | { 260 | "connection_id": "c002", 261 | "segment_id": "s002", 262 | "flight_number": "KL321", 263 | "aircraft_type": "Boeing 747-200", 264 | "travel_class": "business", 265 | "departure_airport": { 266 | "airport_code": "SLC", 267 | "city": "Salt Lake City", 268 | "terminal": "T1", 269 | "gate": "G33" 270 | }, 271 | "arrival_airport": { 272 | "airport_code": "AMS", 273 | "city": "Amsterdam", 274 | "terminal": "T1", 275 | "gate": "G33" 276 | }, 277 | "flight_schedule": { 278 | "departure_time": "2016-01-02T22:45", 279 | "arrival_time": "2016-01-03T17:20" 280 | } 281 | } 282 | ], 283 | "passenger_segment_info": [ 284 | { 285 | "segment_id": "s001", 286 | "passenger_id": "p001", 287 | "seat": "12A", 288 | "seat_type": "Business" 289 | }, 290 | { 291 | "segment_id": "s001", 292 | "passenger_id": "p002", 293 | "seat": "12B", 294 | "seat_type": "Business" 295 | }, 296 | { 297 | "segment_id": "s002", 298 | "passenger_id": "p001", 299 | "seat": "73A", 300 | "seat_type": "World Business", 301 | "product_info": [ 302 | { 303 | "title": "Lounge", 304 | "value": "Complimentary lounge access" 305 | }, 306 | { 307 | "title": "Baggage", 308 | "value": "1 extra bag 50lbs" 309 | } 310 | ] 311 | }, 312 | { 313 | "segment_id": "s002", 314 | "passenger_id": "p002", 315 | "seat": "73B", 316 | "seat_type": "World Business", 317 | "product_info": [ 318 | { 319 | "title": "Lounge", 320 | "value": "Complimentary lounge access" 321 | }, 322 | { 323 | "title": "Baggage", 324 | "value": "1 extra bag 50lbs" 325 | } 326 | ] 327 | } 328 | ], 329 | "price_info": [ 330 | { 331 | "title": "Fuel surcharge", 332 | "amount": "1597", 333 | "currency": "USD" 334 | } 335 | ], 336 | "base_price": "12206", 337 | "tax": "200", 338 | "total_price": "14003", 339 | "currency": "USD" 340 | } 341 | } 342 | } 343 | }; 344 | 345 | callSendAPI(messageData); 346 | } 347 | 348 | 349 | function sendTextMessage(recipientId, messageText) { 350 | var messageData = { 351 | recipient: { 352 | id: recipientId 353 | }, 354 | message: { 355 | text: messageText 356 | } 357 | }; 358 | 359 | callSendAPI(messageData); 360 | } 361 | 362 | 363 | function receivedPostback(event) { 364 | var senderID = event.sender.id; 365 | var recipientID = event.recipient.id; 366 | var timeOfPostback = event.timestamp; 367 | 368 | // The 'payload' param is a developer-defined field which is set in a postback 369 | // button for Structured Messages. 370 | var payload = event.postback.payload; 371 | 372 | // console.log("Received postback for user %d and page %d with payload '%s' " + 373 | // "at %d", senderID, recipientID, payload, timeOfPostback); 374 | 375 | // When a postback is called, we'll send a message back to the sender to 376 | // let them know it was successful 377 | sendTextMessage(senderID, "Postback called"); 378 | } 379 | 380 | 381 | function callSendAPI(messageData) { 382 | var JSONdMessageData = {} 383 | for(var i in messageData){ 384 | JSONdMessageData[i] = JSON.stringify(messageData[i]) 385 | } 386 | payload = JSONdMessageData 387 | payload.access_token = ACCESS_TOKEN 388 | 389 | var options = 390 | { 391 | "method" : "post", 392 | "payload" : payload, 393 | }; 394 | UrlFetchApp.fetch("https://graph.facebook.com/v2.6/me/messages", options); 395 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jonah 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 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * What is this? 2 | This is an implementation of a Facebook Messenger bot written in [[https://developers.google.com/apps-script/][Google Apps Script]] (a javascript runtime in the google cloud). The advantage of using Google Apps Script is it allows GET and POST requests to be made and recieved for free and without having to run your own server. It also allows arbitrary temporal triggers to be set so functions get be run at exact times or on minutely/hourly/daily/weekly intervals. 3 | * How to get started 4 | 1. Go to [[http://script.google.com]] and copy the code in. Save and name the script and go to the cloud icon (5th icon from the left), change the bottom dropdown ("Who has access to the app") to "Anyone, even anonymous", and click "Publish". Copy that URL for use in facebook 5 | 2. Follow Facebook's [[https://developers.facebook.com/docs/messenger-platform/quickstart][Getting Started]] tutorial and, when it asks for the callback function for the webhook, paste the previous url in 6 | 3. After making any changes, the app needs to be update 7 | * Go to the cloud icon again, click the "Versions" dropdown and go the bottom for "New", then click the "Update" button 8 | * Where should I make changes 9 | The main place to make changes is around line 117 with the case statements, enter the keywords you want and change/write the functions you want that keyword to call (the left panel of [[https://developers.facebook.com/docs/messenger-platform/send-api-reference][this page]] has many good example templates, just copy what's between the "message" key into the new function 10 | (More complicated use cases with callbacks and buttons is possible) 11 | --------------------------------------------------------------------------------