├── .gitignore ├── README.md ├── acronyms.md ├── code ├── phonemo │ ├── .gitignore │ ├── .nvmrc │ ├── .twilioserverlessrc │ ├── assets │ │ └── data.private.js │ ├── functions │ │ ├── incoming-call.js │ │ ├── incoming-message.js │ │ ├── start-talk.protected.js │ │ ├── status-handler.protected.js │ │ └── survey-handler.protected.js │ ├── package-lock.json │ └── package.json └── samples │ └── auto-responders │ ├── csharp │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── csharp.csproj │ └── handle.cs │ ├── java │ ├── .gitattributes │ ├── .gitignore │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── app │ │ ├── .classpath │ │ ├── .project │ │ ├── .settings │ │ │ └── org.eclipse.buildship.core.prefs │ │ ├── bin │ │ │ └── main │ │ │ │ └── autoresponder │ │ │ │ └── App.class │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── autoresponder │ │ │ └── App.java │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle │ ├── php │ ├── auto_responder.php │ ├── composer.json │ └── composer.lock │ ├── python │ ├── auto_responder.py │ └── requirements.txt │ └── ruby │ └── auto_responder.rb ├── learning-objectives.md └── notes.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | venv 4 | .DS_Store 5 | vendor 6 | 7 | 8 | # Created by https://www.toptal.com/developers/gitignore/api/dotnetcore 9 | # Edit at https://www.toptal.com/developers/gitignore?templates=dotnetcore 10 | 11 | ### DotnetCore ### 12 | # .NET Core build folders 13 | bin/ 14 | obj/ 15 | 16 | # Common node modules locations 17 | /node_modules 18 | /wwwroot/node_modules 19 | 20 | # End of https://www.toptal.com/developers/gitignore/api/dotnetcore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilio Essentials - Programmable Messaging and Programmable Voice 2 | 3 | This repository contains the [teacher's notes](./notes.md) and the [completed project code](./code/phonemo) for the Twilio Essentials for Programmable Messaging and Programmable Voice course. 4 | -------------------------------------------------------------------------------- /acronyms.md: -------------------------------------------------------------------------------- 1 | PSTN - Public Switched Telephone Network 2 | CPS - Calls Per Second 3 | MSPS - Messaging Segments Per Second 4 | API - Application Programming Interface 5 | HTML - HyperText Markup Language 6 | TwiML - Twilio Markup Language 7 | -------------------------------------------------------------------------------- /code/phonemo/.gitignore: -------------------------------------------------------------------------------- 1 | # Twilio Serverless 2 | .twiliodeployinfo 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | .env.production 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | .parcel-cache 83 | 84 | # Next.js build output 85 | .next 86 | out 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and not Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | 113 | # Stores VSCode versions used for testing VSCode extensions 114 | .vscode-test 115 | 116 | # yarn v2 117 | .yarn/cache 118 | .yarn/unplugged 119 | .yarn/build-state.yml 120 | .yarn/install-state.gz 121 | .pnp.* 122 | -------------------------------------------------------------------------------- /code/phonemo/.nvmrc: -------------------------------------------------------------------------------- 1 | 12 -------------------------------------------------------------------------------- /code/phonemo/.twilioserverlessrc: -------------------------------------------------------------------------------- 1 | { 2 | "commands": {}, 3 | "environments": {}, 4 | "projects": {}, 5 | // "assets": true /* Upload assets. Can be turned off with --no-assets */, 6 | // "assetsFolder": null /* Specific folder name to be used for static assets */, 7 | // "buildSid": null /* An existing Build SID to deploy to the new environment */, 8 | // "createEnvironment": false /* Creates environment if it couldn't find it. */, 9 | // "cwd": null /* Sets the directory of your existing Serverless project. Defaults to current directory */, 10 | // "detailedLogs": false /* Toggles detailed request logging by showing request body and query params */, 11 | // "edge": null /* Twilio API Region */, 12 | // "env": null /* Path to .env file for environment variables that should be installed */, 13 | // "environment": "dev" /* The environment name (domain suffix) you want to use for your deployment */, 14 | // "extendedOutput": false /* Show an extended set of properties on the output */, 15 | // "force": false /* Will run deployment in force mode. Can be dangerous. */, 16 | // "forkProcess": true /* Disable forking function processes to emulate production environment */, 17 | // "functionSid": null /* Specific Function SID to retrieve logs for */, 18 | // "functions": true /* Upload functions. Can be turned off with --no-functions */, 19 | // "functionsFolder": null /* Specific folder name to be used for static functions */, 20 | // "inspect": null /* Enables Node.js debugging protocol */, 21 | // "inspectBrk": null /* Enables Node.js debugging protocol, stops execution until debugger is attached */, 22 | // "legacyMode": false /* Enables legacy mode, it will prefix your asset paths with /assets */, 23 | // "live": true /* Always serve from the current functions (no caching) */, 24 | // "loadLocalEnv": false /* Includes the local environment variables */, 25 | // "loadSystemEnv": false /* Uses system environment variables as fallback for variables specified in your .env file. Needs to be used with --env explicitly specified. */, 26 | // "logCacheSize": null /* Tailing the log endpoint will cache previously seen entries to avoid duplicates. The cache is topped at a maximum of 1000 by default. This option can change that. */, 27 | // "logLevel": "info" /* Level of logging messages. */, 28 | // "logs": true /* Toggles request logging */, 29 | // "ngrok": null /* Uses ngrok to create a public url. Pass a string to set the subdomain (requires a paid-for ngrok account). */, 30 | // "outputFormat": "" /* Output the log in a different format */, 31 | // "overrideExistingProject": false /* Deploys Serverless project to existing service if a naming conflict has been found. */, 32 | // "port": "3000" /* Override default port of 3000 */, 33 | // "production": false /* Promote build to the production environment (no domain suffix). Overrides environment flag */, 34 | // "properties": null /* Specify the output properties you want to see. Works best on single types */, 35 | // "region": null /* Twilio API Region */, 36 | // "runtime": null /* The version of Node.js to deploy the build to. (node10 or node12) */, 37 | // "serviceName": null /* Overrides the name of the Serverless project. Default: the name field in your package.json */, 38 | // "serviceSid": null /* SID of the Twilio Serverless Service to deploy to */, 39 | // "sourceEnvironment": null /* SID or suffix of an existing environment you want to deploy from. */, 40 | // "tail": false /* Continuously stream the logs */, 41 | // "template": null /* undefined */, 42 | } -------------------------------------------------------------------------------- /code/phonemo/assets/data.private.js: -------------------------------------------------------------------------------- 1 | class Data { 2 | constructor(context) { 3 | this.context = context; 4 | } 5 | 6 | getCurrentTalk() { 7 | return this.getUpcomingTalks()[0]; 8 | } 9 | 10 | getUpcomingTalks() { 11 | return [ 12 | { 13 | title: "Billionaires in Space", 14 | code: "astronaut", 15 | startTime: "2021-10-01T17:00:00.038Z", 16 | speakers: [ 17 | { 18 | name: "Craig", 19 | phoneNumber: "+15038368731", 20 | }, 21 | ], 22 | }, 23 | { 24 | title: "Taco Tuesdays should be every day", 25 | code: "taco", 26 | startTime: "2021-10-02T17:00:00.038Z", 27 | speakers: [ 28 | { 29 | name: "Craig", 30 | phoneNumber: "+15038368731", 31 | }, 32 | ], 33 | }, 34 | ]; 35 | } 36 | 37 | getTalkByCode(code) { 38 | const talks = this.getUpcomingTalks(); 39 | return talks.find((talk) => talk.code === code); 40 | } 41 | 42 | addRegistration(code, phoneNumber) { 43 | // NOOP 44 | return true; 45 | } 46 | 47 | async getRegistrants(talk) { 48 | // TODO: This needs to come from an external data source 49 | // NOT the messages log 50 | const client = this.context.getTwilioClient(); 51 | const messages = await client.messages.list({ 52 | to: this.context.TWILIO_PHONE_NUMBER, 53 | }); 54 | return messages 55 | .filter((msg) => { 56 | const action = this.parseInput(msg.body); 57 | return action.command === "join" && action.code === talk.code; 58 | }) 59 | .map((msg) => { 60 | return { phoneNumber: msg.from }; 61 | }); 62 | } 63 | 64 | parseInput(input) { 65 | // eg: join tacos 66 | const action = { 67 | input, 68 | }; 69 | const normalized = input.trim().toLowerCase(); 70 | const parts = normalized.split(/\s+/); 71 | if (parts.length === 2) { 72 | action.command = parts[0]; 73 | action.code = parts[1]; 74 | } 75 | return action; 76 | } 77 | } 78 | 79 | module.exports = { 80 | Data, 81 | }; 82 | -------------------------------------------------------------------------------- /code/phonemo/functions/incoming-call.js: -------------------------------------------------------------------------------- 1 | const assets = Runtime.getAssets(); 2 | const { Data } = require(assets["/data.js"].path); 3 | 4 | exports.handler = (context, event, callback) => { 5 | const twiml = new Twilio.twiml.VoiceResponse(); 6 | const data = new Data(context); 7 | const talk = data.getCurrentTalk(); 8 | if (talk !== undefined) { 9 | twiml.dial().conference( 10 | { 11 | muted: false, 12 | beep: false, 13 | startConferenceOnEnter: false, 14 | }, 15 | talk.code 16 | ); 17 | } else { 18 | // TODO: Build a voice representation of upcoming talks 19 | twiml.say( 20 | "There is no talk currently. Send us a text for a schedule of upcoming talks" 21 | ); 22 | } 23 | console.log(`TwiML is ${twiml}`); 24 | return callback(null, twiml); 25 | }; 26 | -------------------------------------------------------------------------------- /code/phonemo/functions/incoming-message.js: -------------------------------------------------------------------------------- 1 | const assets = Runtime.getAssets(); 2 | const { Data } = require(assets["/data.js"].path); 3 | 4 | exports.handler = (context, event, callback) => { 5 | const twiml = new Twilio.twiml.MessagingResponse(); 6 | const data = new Data(context); 7 | 8 | const action = data.parseInput(event.Body); 9 | switch (action.command) { 10 | case "join": 11 | const talk = data.getTalkByCode(action.code); 12 | if (talk !== undefined) { 13 | data.addRegistration(action.code, event.From); 14 | twiml.message( 15 | `You are now registered for ${talk.title}. Don't call us, we'll call you! 💪📲` 16 | ); 17 | } else { 18 | twiml.message( 19 | `Unable to find upcoming talk with code "${action.code}""` 20 | ); 21 | } 22 | break; 23 | default: 24 | twiml.message(`I'm not sure what you mean by "${event.Body}"`); 25 | break; 26 | } 27 | return callback(null, twiml); 28 | }; 29 | -------------------------------------------------------------------------------- /code/phonemo/functions/start-talk.protected.js: -------------------------------------------------------------------------------- 1 | const assets = Runtime.getAssets(); 2 | const { Data } = require(assets["/data.js"].path); 3 | 4 | exports.handler = async (context, event, callback) => { 5 | const data = new Data(context); 6 | const client = context.getTwilioClient(); 7 | const code = event.TalkCode; 8 | const talk = data.getTalkByCode(code); 9 | const speakerCallSids = []; 10 | console.log("Calling speakers..."); 11 | let domainName = context.DOMAIN_NAME; 12 | if (domainName.startsWith("localhost")) { 13 | domainName = "f4c2-97-120-39-113.ngrok.io"; 14 | } 15 | for (const speaker of talk.speakers) { 16 | const participant = await client 17 | .conferences(talk.code) 18 | .participants.create({ 19 | to: speaker.phoneNumber, 20 | from: context.TWILIO_PHONE_NUMBER, 21 | label: speaker.name, 22 | beep: true, 23 | startConferenceOnEnter: true, 24 | conferenceStatusCallback: `https://${domainName}/status-handler`, 25 | conferenceStatusCallbackEvent: ["leave"], 26 | }); 27 | speakerCallSids.push(participant.callSid); 28 | } 29 | 30 | const registrants = await data.getRegistrants(talk); 31 | console.log("Calling registrants..."); 32 | let registrantPhoneNumbers = registrants.map((r) => r.phoneNumber); 33 | const registrantPhoneNumbersSet = new Set(registrantPhoneNumbers); 34 | registrantPhoneNumbers = [...registrantPhoneNumbersSet]; 35 | 36 | const promises = registrantPhoneNumbers.map(async (registrantPhoneNumber) => { 37 | const participant = await client 38 | .conferences(talk.code) 39 | .participants.create({ 40 | to: registrantPhoneNumber, 41 | from: context.TWILIO_PHONE_NUMBER, 42 | muted: true, 43 | beep: false, 44 | startConferenceOnEnter: false, 45 | }); 46 | return participant.callSid; 47 | }); 48 | 49 | const results = await Promise.allSettled(promises); 50 | const registrantCallSids = results 51 | .filter((result) => result.status === "fulfilled") 52 | .map((result) => result.value); 53 | 54 | results 55 | .filter((result) => result.status === "rejected") 56 | .forEach((result) => console.error(result.reason)); 57 | 58 | console.log(` 59 | Code: ${event.TalkCode} 60 | Speakers: ${speakerCallSids.length} 61 | Registrants: ${registrantCallSids.length} 62 | Failed registrant attempts: ${ 63 | registrantPhoneNumbers.length - registrantCallSids.length 64 | } 65 | `); 66 | callback(null, { 67 | talkCode: event.TalkCode, 68 | speakerCallSids, 69 | registrantCallSids, 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /code/phonemo/functions/status-handler.protected.js: -------------------------------------------------------------------------------- 1 | const assets = Runtime.getAssets(); 2 | const { Data } = require(assets["/data.js"].path); 3 | 4 | exports.handler = async (context, event, callback) => { 5 | const data = new Data(context); 6 | const client = context.getTwilioClient(); 7 | switch (event.StatusCallbackEvent) { 8 | case "participant-leave": 9 | const code = event.FriendlyName; 10 | const talk = data.getTalkByCode(code); 11 | console.log(`Sending text about ${talk.title}`); 12 | const call = await client.calls(event.CallSid).fetch(); 13 | const attendeeNumber = 14 | call.direction === "outbound-api" ? call.to : call.from; 15 | const message = await client.messages.create({ 16 | to: attendeeNumber, 17 | messagingServiceSid: context.MESSAGING_SERVICE_SID, 18 | body: `Thanks for attending ${talk.title}. 19 | On a scale of 1 to 10, how likely are you to recommend PhoneMO to you friends or family`, 20 | }); 21 | break; 22 | default: 23 | console.log(`Unhandled event ${event.StatusCallbackEvent}`); 24 | break; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /code/phonemo/functions/survey-handler.protected.js: -------------------------------------------------------------------------------- 1 | exports.handler = (context, event, callback) => { 2 | const twiml = new Twilio.twiml.MessagingResponse(); 3 | console.log(`Feedback was ${event.Body}`); 4 | twiml.message( 5 | "Thanks for not letting us miss out on your important feedback!" 6 | ); 7 | return callback(null, twiml); 8 | }; 9 | -------------------------------------------------------------------------------- /code/phonemo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phonemo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "twilio-run", 8 | "deploy": "twilio-run deploy" 9 | }, 10 | "dependencies": { 11 | "@twilio/runtime-handler": "1.1.3", 12 | "node-dotenv": "^1.1.1", 13 | "twilio": "^3.56" 14 | }, 15 | "devDependencies": { 16 | "twilio-run": "^3.2.2" 17 | }, 18 | "engines": { 19 | "node": "12" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code/samples/auto-responders/csharp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace csharp 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /code/samples/auto-responders/csharp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:14670", 7 | "sslPort": 44390 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "csharp": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code/samples/auto-responders/csharp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | 11 | using Twilio.TwiML; 12 | using Twilio.TwiML.Messaging; 13 | 14 | namespace csharp 15 | { 16 | public class Startup 17 | { 18 | // This method gets called by the runtime. Use this method to add services to the container. 19 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | } 23 | 24 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 25 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 26 | { 27 | if (env.IsDevelopment()) 28 | { 29 | app.UseDeveloperExceptionPage(); 30 | } 31 | 32 | app.UseRouting(); 33 | 34 | app.UseEndpoints(endpoints => 35 | { 36 | endpoints.MapPost("/auto-responder", async context => 37 | { 38 | var form = context.Request.Form; 39 | var twiml = new MessagingResponse(); 40 | if (form["Body"].ToString().ToUpper().Equals("QUEST")) { 41 | twiml.Message("Discover your power to change the world with code! https://twilio.com/quest"); 42 | } else { 43 | twiml.Message("I don't know what you mean by " + form["Body"] + ". Did you mean QUEST?"); 44 | } 45 | await context.Response.WriteAsync(twiml.ToString()); 46 | }); 47 | 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /code/samples/auto-responders/csharp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /code/samples/auto-responders/csharp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /code/samples/auto-responders/csharp/csharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /code/samples/auto-responders/csharp/handle.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/essentials-messaging-voice-course/dfdf954cc024b8bef241e8a89f5964dc45099e64/code/samples/auto-responders/csharp/handle.cs -------------------------------------------------------------------------------- /code/samples/auto-responders/java/.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | autoresponder 4 | Project java created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | 19 | 1635789160696 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | 25 | 1635789160693 26 | 27 | 30 28 | 29 | org.eclipse.core.resources.regexFilterMatcher 30 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/app/bin/main/autoresponder/App.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/essentials-messaging-voice-course/dfdf954cc024b8bef241e8a89f5964dc45099e64/code/samples/auto-responders/java/app/bin/main/autoresponder/App.class -------------------------------------------------------------------------------- /code/samples/auto-responders/java/app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This generated file contains a sample Java application project to get you started. 5 | * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle 6 | * User Manual available at https://docs.gradle.org/7.2/userguide/building_java_projects.html 7 | */ 8 | 9 | plugins { 10 | // Apply the application plugin to add support for building a CLI application in Java. 11 | id 'application' 12 | } 13 | 14 | repositories { 15 | // Use Maven Central for resolving dependencies. 16 | mavenCentral() 17 | } 18 | 19 | dependencies { 20 | // Use JUnit test framework. 21 | testImplementation 'junit:junit:4.13.2' 22 | 23 | // This dependency is used by the application. 24 | implementation 'com.google.guava:guava:30.1.1-jre' 25 | implementation 'org.slf4j:slf4j-simple:1.7.21' 26 | implementation 'com.twilio.sdk:twilio:8.21.0' 27 | implementation 'com.sparkjava:spark-core:2.9.3' 28 | } 29 | 30 | application { 31 | // Define the main class for the application. 32 | mainClass = 'autoresponder.App' 33 | } 34 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/app/src/main/java/autoresponder/App.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Java source file was generated by the Gradle 'init' task. 3 | */ 4 | package autoresponder; 5 | 6 | import static spark.Spark.*; 7 | 8 | import com.twilio.twiml.messaging.Message; 9 | import com.twilio.twiml.MessagingResponse; 10 | 11 | public class App { 12 | 13 | public static void main(String[] args) { 14 | post("/auto-responder", (req, res) -> { 15 | final String body = req.queryParams("Body"); 16 | System.out.println("Body is:" + body); 17 | res.type("application/xml"); 18 | Message message; 19 | if (body.toUpperCase().equals("QUEST")) { 20 | message = new Message.Builder( 21 | "Discover your power to change the world with code! https://twilio.com/quest").build(); 22 | } else { 23 | message = new Message.Builder("I don't know what you mean by " + body + ". Did you mean QUEST?") 24 | .build(); 25 | } 26 | MessagingResponse twiml = new MessagingResponse.Builder().message(message).build(); 27 | return twiml.toXml(); 28 | 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/essentials-messaging-voice-course/dfdf954cc024b8bef241e8a89f5964dc45099e64/code/samples/auto-responders/java/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /code/samples/auto-responders/java/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /code/samples/auto-responders/java/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/7.2/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'autoresponder' 11 | include('app') 12 | -------------------------------------------------------------------------------- /code/samples/auto-responders/php/auto_responder.php: -------------------------------------------------------------------------------- 1 | message('Discover your power to change the world with code! https://twilio.com/quest'); 10 | } else { 11 | $twiml->message("I do not know what you mean by " . $_POST['Body'] . ". Did you mean QUEST?"); 12 | } 13 | 14 | header('Content-Type: text/xml'); 15 | echo $twiml; 16 | ?> -------------------------------------------------------------------------------- /code/samples/auto-responders/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdennis/php", 3 | "require": { 4 | "twilio/sdk": "^6.30" 5 | }, 6 | "autoload": { 7 | "psr-4": { 8 | "Cdennis\\Php\\": "src/" 9 | } 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Craig Dennis", 14 | "email": "cdennis@twilio.com" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /code/samples/auto-responders/php/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "06e704d4d84f0c10e51590c83ded3147", 8 | "packages": [ 9 | { 10 | "name": "twilio/sdk", 11 | "version": "6.30.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/twilio/twilio-php.git", 15 | "reference": "d60bc2faf10656c100ddfa55d06c33bd8b782957" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/twilio/twilio-php/zipball/d60bc2faf10656c100ddfa55d06c33bd8b782957", 20 | "reference": "d60bc2faf10656c100ddfa55d06c33bd8b782957", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.1.0" 25 | }, 26 | "require-dev": { 27 | "guzzlehttp/guzzle": "^6.3 || ^7.0", 28 | "phpunit/phpunit": ">=4.5", 29 | "theseer/phpdox": "^0.12.0" 30 | }, 31 | "suggest": { 32 | "guzzlehttp/guzzle": "An HTTP client to execute the API requests" 33 | }, 34 | "type": "library", 35 | "autoload": { 36 | "psr-4": { 37 | "Twilio\\": "src/Twilio/" 38 | } 39 | }, 40 | "notification-url": "https://packagist.org/downloads/", 41 | "license": [ 42 | "MIT" 43 | ], 44 | "authors": [ 45 | { 46 | "name": "Twilio API Team", 47 | "email": "api@twilio.com" 48 | } 49 | ], 50 | "description": "A PHP wrapper for Twilio's API", 51 | "homepage": "http://github.com/twilio/twilio-php", 52 | "keywords": [ 53 | "api", 54 | "sms", 55 | "twilio" 56 | ], 57 | "support": { 58 | "issues": "https://github.com/twilio/twilio-php/issues", 59 | "source": "https://github.com/twilio/twilio-php/tree/6.30.0" 60 | }, 61 | "time": "2021-10-18T19:13:07+00:00" 62 | } 63 | ], 64 | "packages-dev": [], 65 | "aliases": [], 66 | "minimum-stability": "stable", 67 | "stability-flags": [], 68 | "prefer-stable": false, 69 | "prefer-lowest": false, 70 | "platform": [], 71 | "platform-dev": [], 72 | "plugin-api-version": "2.1.0" 73 | } 74 | -------------------------------------------------------------------------------- /code/samples/auto-responders/python/auto_responder.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from twilio.twiml.messaging_response import MessagingResponse 3 | 4 | app = Flask(__name__) 5 | 6 | 7 | @app.route("/auto-responder", methods=['POST']) 8 | def auto_responder(): 9 | """Respond to incoming message""" 10 | # Start our TwiML response 11 | twiml = MessagingResponse() 12 | 13 | print(f"Body was {request.values['Body']}") 14 | 15 | if request.values["Body"].upper() == "QUEST": 16 | twiml.message("Discover your power to change the world with code! https://twilio.com/quest") 17 | else: 18 | twiml.message(f"I don't know what you mean by \"{request.values['Body']}\". Did you mean QUEST?") 19 | return str(twiml) 20 | 21 | if __name__ == "__main__": 22 | app.run(debug=True) 23 | -------------------------------------------------------------------------------- /code/samples/auto-responders/python/requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | certifi==2019.6.16 3 | cffi==1.12.3 4 | chardet==3.0.4 5 | click==7.1.2 6 | conda==4.7.10 7 | conda-package-handling==1.3.11 8 | cryptography==2.7 9 | Flask==1.1.1 10 | http-prompt==2.1.0 11 | httpie==2.4.0 12 | idna==2.8 13 | itsdangerous==1.1.0 14 | Jinja2==2.11.2 15 | libarchive-c==2.8 16 | MarkupSafe==1.1.1 17 | parsimonious==0.8.1 18 | prompt-toolkit==1.0.18 19 | pycosat==0.6.3 20 | pycparser==2.19 21 | Pygments==2.8.1 22 | PyJWT==1.7.1 23 | pyOpenSSL==19.0.0 24 | PySocks==1.7.0 25 | pytz==2020.1 26 | PyYAML==5.4.1 27 | requests==2.22.0 28 | requests-toolbelt==0.9.1 29 | ruamel-yaml==0.15.46 30 | six==1.12.0 31 | sockets==1.0.0 32 | tqdm==4.32.1 33 | twilio==6.44.1 34 | urllib3==1.24.2 35 | wcwidth==0.2.5 36 | Werkzeug==1.0.1 37 | -------------------------------------------------------------------------------- /code/samples/auto-responders/ruby/auto_responder.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'twilio-ruby' 3 | 4 | post '/auto-responder' do 5 | Twilio::TwiML::MessagingResponse.new do |twiml| 6 | if params['Body'].upcase == 'QUEST' 7 | twiml.message(body: 'Discover your power to change the world with code! https://twilio.com/quest') 8 | else 9 | twiml.message(body: "I don't know what you mean by \"#{params['Body']}\". Did you mean QUEST?") 10 | end 11 | end.to_s 12 | end 13 | -------------------------------------------------------------------------------- /learning-objectives.md: -------------------------------------------------------------------------------- 1 | ## Unit 1 - Introducing Twilio and Programmable Messaging 2 | 3 | ### Video 1 - Welcome 4 | 5 | ### Video 2 - Signing up 6 | 7 | * Understand that trial account has limitations 8 | * Obtain a trial number 9 | 10 | ### Video 3 - What even is a message? 11 | 12 | * Identify that SMS stands for Short Messaging Service 13 | * Identify that MMS stands for Multimedia Messaging Service 14 | * Recall that SMS messages are built of 160 character segments 15 | * Contrast SMS urgency with email's preferability 16 | * Use the documentation to explore code samples 17 | 18 | ### Video 4 - Send a text message 19 | 20 | * Use the Twilio CLI to send an outbound SMS message 21 | * Use the Twilio CLI to send an MMS message 22 | * Recall that multi-language helper library code samples are available in the documentation 23 | 24 | ### Video 5 - Receiving a message 25 | 26 | * Recite that TwIML stands for Twilio Markup Language 27 | * Recall that TwiML is tag based and case-sensitive 28 | * Use a TwiML Bin for static responses 29 | * Change incoming message handler to return static TwiML 30 | * Create a static auto responder 31 | 32 | ### Video 6 - Responding dynamically 33 | 34 | * State that a Webhook is a method to pass control to a web based application 35 | * Use a Twilio Function for dynamic content 36 | * Use the Twilio helper library to generate TwiML 37 | * Recall that you can output debugging information in a Twilio Function using console.log 38 | * Change the incoming message configuration to point to a deployed Twilio Function 39 | 40 | ### Video 7 - Review + Practice 41 | * Recognize that regulations vary based on region as to what is allowed 42 | * State that there are tools like Twilio Studio that can help to manage statelessness 43 | 44 | ## Unit 2 - Programmable Voice 45 | 46 | ### Video 1 - Appreciating the Phone 47 | 48 | * Recall that voice is programmable with reusable building code blocks 49 | * Recognize that PSTN stands for Public Switched Telephone Network 50 | 51 | ### Video 2 - Receive a Call 52 | 53 | * Modify settings on the phone number to handle incoming calls 54 | * Use in a TwiML response to perform text-to-speech. 55 | * Use in a TwiML response to play an audio file 56 | 57 | ### Video 3 - Gather Input 58 | 59 | * Understand that is used to gather DTMF (Dual Tone Multi Frequency) 60 | * Explain that the URL in the action parameter on the will receive the Digits parameter in an HTTP request. 61 | * Use a nested in to perform a barge-in (interrupt the audio) 62 | * Recall that context object in a Function handler has access to environment variables 63 | 64 | ### Video 4 - Create an Outbound Call 65 | 66 | * Understand that there is a Call REST API Resource that represents a call. 67 | * Use the debugger webhook to be notified of errors in real-time 68 | * Restate that the default limit is one call per second 69 | 70 | ### Video 5 - Review + Practice 71 | 72 | * Recall that there are client-side SDKs for use in websites and mobile applications 73 | 74 | ## Unit 3 - All Together Now 75 | 76 | ### Video 1 - Project Introduction 77 | 78 | ### Video 2 - Use the Serverless Toolkit 79 | 80 | * Use environment variables to store secrets 81 | * Recall that the context parameter in the Twilio Function handler will provide access to the environment variables 82 | * Use the CLI to update your phone numbers incoming webhook 83 | * Understand that local webhook development is possible using a tool like ngrok to open up a tunnel 84 | * Restate that ngrok allows you to inspect requests from Twilio 85 | 86 | ### Video 3 - Create a Conference 87 | 88 | * Restate that ing a Conference by name will create a new conference if there is not an active conference with that name 89 | * Update incoming voice handler using the Twilio CLI 90 | * Recall that the participant list of a Conference contains active calls only 91 | 92 | ### Video 4 - Use Private Data 93 | 94 | * Produce a private asset and import it into a Function 95 | 96 | ### Video 5 - Allow for registration via SMS 97 | 98 | * Set incoming message handler via the CLI 99 | * Recall how to produce a dynamic flow based on incoming SMS 100 | 101 | ### Video 6 - Call the registrants 102 | 103 | * Use the add participant Conference API to create an outbound call 104 | * Recognize the asynchronous promise pattern when using JavaScript array methods 105 | * Recall that the default of 1 outbound Call Per Second can be changed 106 | 107 | ### Video 7 - Deploy 108 | 109 | * Deploy a Twilio Application using the Serverless Toolkit 110 | * Restate the X-Twilio-Signature header is used to protect Webhooks by signing with an auth token 111 | * Create an X-Twilio-Signature header using the JavaScript helper library 112 | 113 | ### Video 8 - Send a follow-up survey 114 | 115 | * Recognize Messaging Services as a recommended best practice to deal with regulations 116 | * Define a Messaging Service as a container for numbers and feature configuration 117 | * Create and wire up a protected function to handle incoming text messages 118 | 119 | ### Video 9 - Wrap-up 120 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | # Twilio Essentials - Programmable Messaging and Programmable Voice 2 | 3 | ## Unit 1 - Introducing Twilio and Programmable Messaging 4 | 5 | ### Video 1 - Welcome 6 | 7 | - 📽 [Introduction to APIs](https://www.youtube.com/watch?v=GZvSYJDk-us) 8 | 9 | ### Video 2 - Signing up 10 | 11 | ### Video 3 - What even is a message? 12 | 13 | - 📚 [Message Resource API](https://www.twilio.com/docs/sms/api/message-resource) 14 | - 🧰 [Message Segmenting Calculator](https://twiliodeved.github.io/message-segment-calculator/) 15 | - 👀 [WhatsApp Business API](https://www.twilio.com/whatsapp) 16 | 17 | ### Video 4 - Send a text message 18 | 19 | - 📚 [Twilio CLI](https://twil.io/cli) 20 | - 🙋‍♂️ [Support - Adding a verified phone number](https://support.twilio.com/hc/en-us/articles/223180048-Adding-a-Verified-Phone-Number-or-Caller-ID-with-Twilio) 21 | - 📚 [Twilio CLI - JSON output format](https://www.twilio.com/docs/twilio-cli/general-usage#json-output-format) 22 | 23 | ### Video 5 - Receiving a message 24 | 25 | - 📚 [``](https://www.twilio.com/docs/messaging/twiml/message) 26 | - 🎮 [Play TwilioQuest](https://twilio.com/quest) 27 | - 📚 [Twilio Webhooks](https://www.twilio.com/docs/usage/webhooks) 28 | - 📽 [Understanding Webhooks - freeCodeCamp](https://www.youtube.com/watch?v=41NOoEz3Tzc) 29 | 30 | #### TwiML Bin: TwilioQuest 31 | 32 | ```xml 33 | 34 | 35 | Discover your power to change the world with code! https://twilio.com/quest 36 | 37 | ``` 38 | 39 | ### Video 6 - Responding dynamically 40 | 41 | - 👀 [Handle POST data in other web frameworks - MDN](https://developer.mozilla.org/en-US/docs/Learn/Forms/Sending_and_retrieving_form_data#on_the_server_side_retrieving_the_data) 42 | 43 | #### /auto-responder 44 | 45 | ```javascript 46 | exports.handler = function (context, event, callback) { 47 | console.log(`Body was ${event.Body}`); 48 | const twiml = new Twilio.twiml.MessagingResponse(); 49 | if (event.Body.toUpperCase() === "QUEST") { 50 | twiml.message( 51 | "Discover your power to change the world with code! https://twilio.com/quest" 52 | ); 53 | } else { 54 | twiml.message( 55 | `I don't know what you mean by "${event.Body}". Did you mean QUEST?` 56 | ); 57 | } 58 | console.log(`TwiML: ${twiml}`); 59 | return callback(null, twiml); 60 | }; 61 | ``` 62 | 63 | ### Video 7 - Review + Practice 64 | 65 | - 📚 [Helper Libraries](https://www.twilio.com/docs/libraries) 66 | - 📚 [Messaging Services Best Practices at Scale](https://www.twilio.com/docs/messaging/guides/best-practices-at-scale) 67 | - 🙋‍♂️ [Support - Adding a verified phone number](https://support.twilio.com/hc/en-us/articles/223180048-Adding-a-Verified-Phone-Number-or-Caller-ID-with-Twilio) 68 | 69 | #### Practice Programmable Messaging 70 | 71 | Here are some practice and example applications for you to experiment with. Let us know how it's going [@TwilioDevs](https://twitter.com/TwilioDevs) or in the [community](https://community.twilio.com) 72 | 73 | - 💡 Create an SMS auto-responder that shares a link to one of your favorite web applications 74 | - 💡 Build a dynamic SMS based [magic 8-ball](https://en.wikipedia.org/wiki/Magic_8-Ball) app using Twilio Functions 75 | - 🧪 [Build SMS Appointment Reminders](https://www.twilio.com/docs/sms/tutorials/appointment-reminders) 76 | - 👩‍💻 [CodeExchange - SMS Forwarding](https://www.twilio.com/code-exchange/simple-sms-forwarding) 77 | - 👩‍💻 [CodeExchange - Send browser based SMS notifications](https://www.twilio.com/code-exchange/browser-based-sms-notifications) 78 | 79 | ## Unit 2 - Programmable Voice 80 | 81 | ### Video 1 - Appreciating the Phone 82 | 83 | - 👀 [Ahoy - Wikipedia]() 84 | - 👩‍💻 [twilio.com/ahoy - All things developer](https://twilio.com/ahoy) 85 | 86 | ### Video 2 - Receive a Call 87 | 88 | - 📚 [``](https://www.twilio.com/docs/voice/twiml/say) 89 | - 📚 [Text to Speech - Amazon Polly](https://www.twilio.com/docs/voice/twiml/say/text-speech#amazon-polly) 90 | - 📚 [``](https://www.twilio.com/docs/voice/twiml/play) 91 | - 👀 [Rickrolling](https://en.wikipedia.org/wiki/Rickrolling) 92 | 93 | #### TwiML Bin: Ahoy World 94 | 95 | ```xml 96 | 97 | 98 | Ahoy World! 99 | 100 | ``` 101 | 102 | #### TwiML Bin: Ahoy Rick 103 | 104 | ```xml 105 | 106 | 107 | And now an important message: 108 | https://twil.io/professional-recording-example 109 | 110 | ``` 111 | 112 | ### Video 3 - Gather Input 113 | 114 | - 📚 [``](https://www.twilio.com/docs/voice/twiml/gather) 115 | - 📚 [Queueing Calls](https://www.twilio.com/docs/voice/queue-calls) 116 | - 👀 [Twilio Studio](https://www.twilio.com/studio) 117 | - 📽 [Speed up your Development Process with Twilio Studio](https://www.youtube.com/watch?v=14FXnUgrZ6w) 118 | 119 | #### TwiML Bin: Ahoy World (featuring Gather) 120 | 121 | ```xml 122 | 123 | 124 | And now an important message: 125 | 126 | Press 1 to approve of this joke, Press 2 to talk to a manager 127 | https://twil.io/professional-recording-example 128 | 129 | You listened to the whole song! Way to never give up! 130 | 131 | ``` 132 | 133 | #### Function: /handle-feedback 134 | 135 | ```javascript 136 | exports.handler = function (context, event, callback) { 137 | const twiml = new Twilio.twiml.VoiceResponse(); 138 | console.log(`User pressed ${event.Digits}`); 139 | if (event.Digits === "1") { 140 | twiml.say("That's great! Glad we didn't let you down!"); 141 | } else if (event.Digits === "2") { 142 | //TODO: Look up user from phone number => event.From 143 | const name = "Karen"; 144 | twiml.say( 145 | `Thank you for reporting this joke, ${name}, connecting you with a supervisor` 146 | ); 147 | twiml.enqueue("managers"); 148 | } else { 149 | twiml.say(`You pressed ${event.Digits}...but why?`); 150 | } 151 | return callback(null, twiml); 152 | }; 153 | ``` 154 | 155 | #### TwiML Bin: Get the next caller in the queue 156 | 157 | If you had a phone number you could set a dial in number to use the following TwiML Bin on incoming calls. You'd then just have the manager call this number and they'd get the next caller in the queue! 158 | 159 | ```xml 160 | 161 | 162 | managers 163 | 164 | 165 | ``` 166 | 167 | ### Video 4 - Create an Outbound Call 168 | 169 | - 📚 [Twilio CLI](https://twil.io/cli) 170 | - 👩‍💻 [Twilio CLI Debugger Plugin](https://github.com/twilio/plugin-debugger) 171 | - 📚 [Call Resource Status Callback](https://www.twilio.com/docs/voice/api/call-resource#statuscallback) 172 | - 📚 [Messaging Status Callback pattern](https://www.twilio.com/docs/usage/webhooks/sms-webhooks#type-2-status-callbacks) 173 | - 📚 [Answering Machine Detection](https://www.twilio.com/docs/voice/answering-machine-detection) 174 | - 👩‍💻 [Modify Calls in Progress - Tutorial](https://www.twilio.com/docs/voice/tutorials/how-to-modify-calls-in-progress) 175 | 176 | #### Function: /status-displayer 177 | 178 | ```javascript 179 | exports.handler = function (context, event, callback) { 180 | console.log(`Call: ${event.CallSid} status ${event.CallStatus}`); 181 | return callback(null); 182 | }; 183 | ``` 184 | 185 | ### Video 5 - Review + Practice 186 | 187 | - 📚 [`` `input` attribute](https://www.twilio.com/docs/voice/twiml/gather#input) 188 | - 📚 [Answering Machine Detection](https://www.twilio.com/docs/voice/answering-machine-detection) 189 | - 🙋‍♂️ [Sending and Receiving Limitations on Calls and SMS Messages](https://support.twilio.com/hc/en-us/articles/223183648-Sending-and-Receiving-Limitations-on-Calls-and-SMS-Messages) 190 | - 👀 [Media Streams - Build realtime voice applications](https://www.twilio.com/media-streams) 191 | - 👀 [SIP Interface - Programmatic Voice over Internet or Voip calls](https://www.twilio.com/sip-interface) 192 | - 👀 [Elastic SIP Trunking](https://www.twilio.com/sip-trunking) 193 | - 👀 [Voice SDK - Client side (browser/mobile) voice applications](https://www.twilio.com/voice-sdk) 194 | 195 | #### Practice Programmable Voice 196 | 197 | Here are some practice and example applications for you to experiment with. Let us know how it's going [@TwilioDevs](https://twitter.com/TwilioDevs) or in the [community](https://community.twilio.com) 198 | 199 | - 💡 Using a Twilio Function as an incoming call handler, create a call-in hotline that reports the weather using a [weather API](https://openweathermap.org/api) 200 | - 💡 Create an answering machine using the [` TwiML verb`](https://www.twilio.com/docs/voice/twiml/record) that texts you the transcription via the [`transcribeCallback`](https://www.twilio.com/docs/voice/twiml/record#attributes-transcribe-callback) 201 | - 🧪 [Tutorial - How to Build Call Forwarding](https://www.twilio.com/docs/voice/tutorials/call-forwarding) 202 | - 👩‍💻 [CodeExchange - Basic IVR](https://www.twilio.com/code-exchange/basic-ivr) 203 | - 👩‍💻 [CodeExchange - Pin Protected Conference Line](https://www.twilio.com/code-exchange/pin-protected-conference-line) 204 | 205 | ## Unit 3 - All Together Now 206 | 207 | You can view [all the completed PhoneMO source code](./code/phonemo). 208 | 209 | ### Video 1 - Project Introduction 210 | 211 | - 🎁 Looking for a promo code to upgrade your account out of trial mode? Send me a text message with the word `promo` in it. 212 | - 👀 [Most Popular Social Audio Apps in 2021](https://www.highfidelity.com/blog/most-popular-social-audio-apps) 213 | 214 | ### Video 2 - Use the Serverless Toolkit 215 | 216 | - 📚 [Getting Started with the Serverless Toolkit](https://www.twilio.com/docs/labs/serverless-toolkit/getting-started) 217 | - 🧰 [ngrok](https://ngrok.com/) 218 | 219 | ### Video 3 - Create a Conference 220 | 221 | - 📚 [Voice Conference](https://www.twilio.com/docs/voice/conference) 222 | 223 | ### Video 4 - Use Private Data 224 | 225 | ⏰⏰⏰⏰⏰⏰ 226 | 227 | Whoops! I mistakenly put `muted: false` but I meant `muted: true`! We want callers that aren't the speakers to be silent. Sorry about that! 228 | 229 | ⏰⏰⏰⏰⏰⏰ 230 | 231 | - 📚 [Using Private Assets](https://www.twilio.com/docs/runtime/assets#using-private-assets) 232 | - 👀 [String.prototype.split - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) 233 | 234 | ### Video 5 - Allow for registration via SMS 235 | 236 | - 👀 [switch statement - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch) 237 | 238 | #### Code for showing the schedule 239 | 240 | ```javascript 241 | // Keep track of when a user does something unexpected and then flip the a showHelp boolean 242 | if (showHelp) { 243 | const talks = data.getUpcomingTalks(); 244 | const options = talks.map(talk => `For ${talk.title}: join ${talk.code}`); 245 | twiml.message(options.join("\n")); 246 | } 247 | ``` 248 | 249 | ### Video 6 - Call the registrants 250 | 251 | - ⚙️ [Console - Change your CPS - Calls Per Second](https://console.twilio.com/?frameUrl=/console/voice/settings) 252 | - 👀 [JavaScript Promises - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 253 | - 👀 [JavaScript async functions - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) 254 | 255 | ### Video 7 - Deploy 256 | 257 | - 📚 [Validating Signatures from Twilio](https://www.twilio.com/docs/usage/webhooks/webhooks-security#validating-signatures-from-twilio) 258 | - 📚 [Read only services and editing in the new Functions UI](https://www.twilio.com/docs/runtime/read-only-services-and-editing-functions-ui) 259 | 260 | ### Video 8 - Send a follow-up survey 261 | 262 | - 👀 [SMS Guidelines](https://www.twilio.com/guidelines/sms) 263 | - 👀 [What is Net Promoter Score or NPS](https://www.netpromoter.com/know/) 264 | 265 | ### Video 9 - Wrap-up 266 | 267 | #### Practice 268 | 269 | - 📲 Give me a call or send me a text +15038368731 and let me know how you felt about the course! I used Studio to build both the Messaging and Voice flows. Import the flows. 270 | - 💡 Build an entirely Serverless Voice Mail application! Wire up the incoming number to a Twilio Function that uses the [``](https://www.twilio.com/docs/voice/twiml/record) verb. Turn on transcription and use the webhook to send the transcription to a number specified in an environment variable. 271 | - 👩‍💻 [CodeExchange - Call Forwarding with Voicemail](https://www.twilio.com/code-exchange/call-forwarding-voicemail) 272 | 273 | #### Ideas for the finishing touches for PhoneMO 274 | 275 | - [ ] 💡 Create a [client-side browser based phone](https://www.twilio.com/docs/voice/sdks/javascript) so anyone can listen in from the website 276 | - [ ] 💡 Host a static web page using [Twilio Assets](https://www.twilio.com/docs/runtime/assets) and expose the data through APIs to display the schedule of upcoming talks 277 | --------------------------------------------------------------------------------