twilio
本文由Marc Towler和Bruno Mota进行同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
在本教程中,我们将使用Node.js构建SMS提醒应用程序。 我们将使用用户的Google日历获取约会,然后使用Twilio发送短信。
与以往一样,您可以从github repo中找到本教程中使用的代码。
设置事情
首先,您需要拥有一个Google帐户和一个Twilio帐户。 如果您还没有这些,可以继续注册。 这里是链接:
您无需担心Twilio,可以免费尝试。
Google控制台项目
拥有Google帐户后,请转到Google控制台并创建一个新应用。 默认情况下,Google控制台页面会向您显示您使用过的最新应用程序的仪表板。 但是,如果您还没有从事任何项目,它将显示以下内容:
在这里,您可以单击右上角的“ select project
菜单,然后选择“ create a project
。 这将打开一个模态窗口,允许您输入项目的标题。
创建项目后,将显示仪表板。 您可以从那里单击use Google APIs
,搜索Google Calendar API并启用它。
启用API后,它将要求您创建凭据。 单击Go to Credentials
以开始设置。 这将显示以下内容:
单击Add credentials
按钮,然后选择OAuth 2.0 client ID
。
这将要求您首先配置同意屏幕。 单击“ configure consent screen
。
输入Product name shown to users
的Product name shown to users
文本字段的值,然后单击save
。
配置完成后,您现在可以创建客户端ID。 选择Web application
作为应用程序类型,保留默认名称(如果需要),输入http://localhost:3000/login
作为Authorized redirect URIs
然后单击create
。
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
这将打开一个显示客户端ID和客户端密码的模式。 请暂时注意这些内容,我们稍后将使用它们。
特威里奥
创建Twilio帐户后,请转到设置页面 ,并AuthToken
下Live API Credentials
下AccountSID
和AuthToken
的值。
接下来转到programmable voice dashboard
。 在这里您可以看到沙箱编号。 您可以使用此数字来测试twilio。 但是稍后您将需要购买电话号码,以便twilio发送的短信不会添加到“从twilio沙箱发送”中 。 Twilio沙盒编号的另一个限制是只能与经过验证的编号一起使用。 这意味着您必须向twilio注册电话号码才能向其发送消息。 您可以从“ manage caller IDs page
执行此操作。
构建应用
现在我们准备构建该应用程序。 在继续之前,我想简要介绍一下我们将如何实现该应用程序。 将有三个主要文件:一个用于服务器,一个用于缓存Google日历中的事件,另一个用于提醒用户。 该服务器用于允许用户登录并获得访问令牌。 事件将保存在MySQL数据库中,全局应用程序配置将添加到.json
文件中。 Node的cron
实现将用于执行用于缓存事件和提醒用户的任务。
安装依赖项
在您的工作目录上,创建一个package.json
文件并添加以下内容:
{
"name": "google-calendar-twilio",
"version": "0.0.1",
"dependencies": {
"config": "^1.17.1",
"cron": "^1.1.0",
"express": "^4.13.3",
"googleapis": "^2.1.6",
"moment": "^2.10.6",
"moment-timezone": "^0.4.1",
"mysql": "felixge/node-mysql",
"twilio": "^2.6.0"
}
}
在此文件中,我们指定应用程序所依赖的库的名称和版本。 以下是每个库的用法分类:
-
config
–用于存储和检索全局应用程序配置。 -
cron
–用于在一天的特定时间执行特定任务。 在此应用中,我们使用它来运行任务以缓存用户Google日历中的事件并发送文本提醒。 -
express
– Node.js的事实上的Web框架。 我们正在使用它来提供登录页面。 -
googleapis
– Google API的官方Node.js客户端。 -
moment
–日期和时间库。 我们正在使用它来轻松格式化从Google Calendar API获得的日期。 -
moment-timezone
–时区插件。 这将设置应用程序的默认时区。 -
mysql
– Node.jsMySQL客户端。 -
twilio
– Node.js的官方Twilio客户端。 这使我们可以发送文本提醒。
从终端执行npm install
以安装所有依赖项。
数据库
如前所述,我们将为此应用程序使用MySQL数据库。 继续使用您选择的数据库管理工具创建一个新数据库。 然后,使用以下SQL转储文件创建表: appointment-notifier.sql
。
数据库中有两个表: users
和appointments
。 users
表用于存储用户数据。 对于此应用程序,我们将只存储一个用户,并且仅存储访问令牌。
appointments
表用于存储我们从Google Calendar API获取的事件。 请注意,其中没有user_id
字段,因为我们只有一个用户。 然后,我们将获取所有零行,作为notified
字段的值。
应用程式设定
在您的工作目录上,创建一个config
文件夹,然后在其中创建default.json
文件。 这是我们放置全局应用程序配置的地方。 这包括时区,我们将向其发送提醒的电话号码,数据库,google app和Twilio设置。
这是模板,请务必填写所有字段。
{
"app": {
"timezone": "Asia/Manila"
},
"me": {
"phone_number": ""
},
"db": {
"host": "localhost",
"user": "root",
"password": "secret",
"database": "calendar_notifier"
},
"google":{
"client_id": "THE CLIENT ID OF YOUR GOOGLE APP",
"client_secret": "THE CLIENT SECRET OF YOUR GOOGLE APP",
"redirect_uri": "http://localhost:3000/login",
"access_type": "offline",
"scopes": [
"https://www.googleapis.com/auth/plus.me",
"https://www.googleapis.com/auth/calendar"
]
},
"twilio": {
"sid": "YOUR TWILIO SID",
"secret": "YOUR TWILIO SECRET",
"phone_number": "+YOUR TWILIO PHONE NUMBER / SANDBOX NUMBER"
}
}
共同文件
作为优秀的开发人员,我们需要尽可能避免代码重复。 这就是为什么我们需要将我前面提到的三个主要文件(服务器,缓存,通知)所需的代码放入单独的文件中。 创建一个common
的工作目录文件夹。 这是我们要添加公用文件的地方。
数据库
在common
目录中创建一个db.js
文件,然后添加以下内容:
var config = require('config');
var db_config = config.get('db');
var mysql = require('mysql');
var connection = mysql.createConnection({
host: db_config.host,
user: db_config.user,
password: db_config.password,
database: db_config.database
});
exports.db = connection;
这使用config库获取我们之前在config/default.json
文件中添加的配置值。 具体来说,我们正在获取数据库配置,以便我们可以连接到数据库。 然后,我们将导出此模块,以便以后可以从另一个文件中使用它。
时间
time.js
文件用于通过moment-timezone
库设置默认时moment-timezone
。 我们还导出了时区的值,因为稍后将在运行两个cron任务(缓存事件和通知用户)时使用该值。
var config = require('config');
var app_timezone = config.get('app.timezone');
var moment = require('moment-timezone');
moment.tz.setDefault(app_timezone);
exports.config = {
timezone: app_timezone
};
exports.moment = moment;
谷歌
google.js
文件用于初始化Google客户端和OAuth2客户端。 为了初始化OAuth2客户端,我们需要传入客户端ID,客户端密码和我们先前在配置文件中添加的重定向URL。 然后,我们初始化Google日历服务。 最后,我们导出OAuth2客户端,日历和Google配置。
var config = require('config');
var google_config = config.get('google');
var google = require('googleapis');
var OAuth2 = google.auth.OAuth2;
var oauth2Client = new OAuth2(google_config.client_id, google_config.client_secret, google_config.redirect_uri);
var calendar = google.calendar('v3');
exports.oauth2Client = oauth2Client;
exports.calendar = calendar;
exports.config = google_config;
创建服务器
现在我们准备在服务器上工作。 服务器负责获取访问令牌。 无需用户当前登录即可用于与Google Calendar API通话。首先创建一个server.js
文件并添加以下内容:
var google = require('./common/google');
var connection = require('./common/db');
var express = require('express');
var app = express();
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
function updateAccessToken(tokens, response){
connection.db.query(
"UPDATE users SET access_token = ? WHERE id = 1",
[JSON.stringify(tokens)],
function(err, rows, fields){
if(!err){
console.log('updated!');
response.send('connected!');
}else{
console.log('error updating table');
console.log(err);
response.send('error occured, please try again');
}
}
);
}
app.get('/', function(req, res){
var url = google.oauth2Client.generateAuthUrl({
access_type: google.config.access_type,
scope: google.config.scopes
});
res.send('<a href="' + url + '">login to google</a>');
});
app.get('/login', function(req, res){
var code = req.query.code;
console.log('login');
google.oauth2Client.getToken(code, function(err, tokens){
if(!err){
console.log('tokens');
console.log(tokens);
updateAccessToken(tokens, res);
}else{
res.send('error getting token');
console.log('error getting token');
}
});
});
分解:
首先,我们导入之前创建的google
和db
模块。
var google = require('./common/google');
var connection = require('./common/db');
创建在本地主机的端口3000上运行的Express服务器。 这就是为什么我们之前在应用程序配置和Google的重定向URI中添加了http://localhost:3000/login
:
var express = require('express');
var app = express();
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
定义一个updateAccessToken
函数。 这接受两个参数: tokens
和response
。 令牌是用户授予必要权限后从Google获得的访问令牌。 response
是来自Express的响应对象。 我们将其传递给此函数,以便我们可以向用户发送响应。 在函数内部,我们更新第一行的access_token
。 如前所述,此应用仅适用于单个用户。 一旦access_token
更新,我们将发送响应。
function updateAccessToken(tokens, response){
connection.db.query(
"UPDATE users SET access_token = ? WHERE id = 1",
[JSON.stringify(tokens)],
function(err, rows, fields){
if(!err){
console.log('updated!');
response.send('connected!');
}else{
console.log('error updating table');
console.log(err);
response.send('error occured, please try again');
}
}
);
}
添加主页的路由。 当访问http://localhost:3000
时,将执行此命令。 从这里我们生成身份验证URL。 这使用来自oauth2Client
的generateAuthUrl
方法。 它接受包含access_type
和scope
的对象。 我们将从先前创建的应用程序配置文件中获取这些值。 最后,我们发送用户可以单击的实际链接。 注意,您应该始终在视图内执行此操作,但是为了简化操作,我们将直接返回链接。
app.get('/', function(req, res){
var url = google.oauth2Client.generateAuthUrl({
access_type: google.config.access_type,
scope: google.config.scopes
});
res.send('<a href="' + url + '">login to google</a>');
});
添加登录路径。 这是在为用户赋予必要的权限后重定向用户的途径。 Google传递了一个名为code
的查询参数。 我们通过请求中的query
对象来获取它。 然后,我们调用getToken
方法并将code
作为参数传递。 这将为我们提供访问令牌。 因此,我们调用updateAccessToken
函数将其保存到数据库中。
app.get('/login', function(req, res){
var code = req.query.code;
console.log('login');
google.oauth2Client.getToken(code, function(err, tokens){
if(!err){
console.log('tokens');
console.log(tokens);
updateAccessToken(tokens, res);
}else{
res.send('error getting token');
console.log('error getting token');
}
});
});
创建Cacher
缓存器负责将用户约会保存到数据库中。 这样可以避免我们每次发送提醒时都必须查询Google Calendar API目录。 创建一个cache.js
文件并添加以下内容:
var google = require('./common/google');
var connection = require('./common/db');
var time = require('./common/time');
var CronJob = require('cron').CronJob;
function addAppointment(event_id, summary, start, end){
connection.db.query(
"INSERT INTO appointments SET id = ?, summary = ?, datetime_start = ?, datetime_end = ?, notified = 0",
[event_id, summary, start, end],
function(err, rows, fields){
if(!err){
console.log('added!');
}else{
console.log('error adding to table');
}
}
);
}
function getEvents(err, response){
console.log('response');
console.log(response);
if(err){
console.log('The API returned an error: ' + err);
}
var events = response.items;
if(events.length == 0){
console.log('No upcoming events found.');
}else{
console.log('Upcoming 10 events:');
for(var i = 0; i < events.length; i++){
var event = events[i];
var event_id = event.id;
var summary = event.summary;
var start = event.start.dateTime || event.start.date;
var end = event.end.dateTime || event.end.date;
addAppointment(event_id, summary, start, end);
}
}
}
function cache(){
var current_datetime = time.moment().toISOString();
google.calendar.events.list({
auth: google.oauth2Client,
calendarId: 'primary',
timeMin: current_datetime,
maxResults: 10,
singleEvents: true,
orderBy: 'startTime'
}, getEvents);
}
connection.db.query('SELECT access_token FROM users WHERE id = 1', function(error, results, fields){
if(!error){
var tokens = JSON.parse(results[0].access_token);
google.oauth2Client.setCredentials({
'access_token': tokens.access_token,
'refresh_token': tokens.refresh_token
});
new CronJob('0 0 * * *', cache, null, true, time.config.timezone);
//cache(); //for testing
}
});
分解:
首先,我们导入所需的所有模块。
var google = require('./common/google');
var connection = require('./common/db');
var time = require('./common/time');
var CronJob = require('cron').CronJob;
addAppointment
函数负责将约会保存到appointments
表中。 这接受约会的event_id
, summary
, start
和end
日期时间。 event_id
基本上是Google日历中特定约会的ID。 我们将其用作主键的值,这意味着重复项不会插入到appointments
表中。 这里缺少的是比较数据库中已有的约会和API返回的约会的方法。 如果由于某种原因约会的时间表发生了变化,那么数据库将不会被更新,因为我们在这里所做的就是将其插入到表中。 我将其留给您的待办事项清单。
function addAppointment(event_id, summary, start, end){
connection.db.query(
"INSERT INTO appointments SET id = ?, summary = ?, datetime_start = ?, datetime_end = ?, notified = 0",
[event_id, summary, start, end],
function(err, rows, fields){
if(!err){
console.log('added!');
}else{
console.log('error adding to table');
}
}
);
}
getEvents
函数负责遍历API返回的所有约会。 这使用addAppointment
方法保存循环的每次迭代的约会。
function getEvents(err, response){
console.log('response');
console.log(response);
if(err){
console.log('The API returned an error: ' + err);
}
var events = response.items;
if(events.length == 0){
console.log('No upcoming events found.');
}else{
for(var i = 0; i < events.length; i++){
var event = events[i];
var event_id = event.id;
var summary = event.summary;
var start = event.start.dateTime || event.start.date;
var end = event.end.dateTime || event.end.date;
addAppointment(event_id, summary, start, end);
}
}
}
cache
方法是实际调用Google Calendar API的方法。 这是通过使用Google客户端来实现的。 在这里,我们在calendar.events
对象上调用list
方法。 它接受两个参数:第一个是包含查询选项的对象,第二个是返回结果后要执行的函数。
function cache(){
var current_datetime = time.moment().toISOString();
google.calendar.events.list({
auth: google.oauth2Client,
calendarId: 'primary',
timeMin: current_datetime,
maxResults: 10,
singleEvents: true,
orderBy: 'startTime'
}, getEvents);
}
在包含选项的对象中,我们具有以下内容:
-
auth
–这是oauth2Client
。 这用于验证请求。 -
calendarId
–我们将在其中获取约会的日历的ID。 在这种情况下,我们使用的是主日历。 Google日历实际上允许您创建许多日历。 其他人也可以与您共享日历。 每个日历都有自己的ID。 这就是我们在此处指定的内容。 如果您有兴趣访问其他日历,请务必查看Calendars上的API文档 。 -
timeMin
–查询中要使用的基本日期时间。 在这种情况下,我们使用的是当前日期时间。 因为谁想知道过去发生的事件? 请注意,尽管这使用ISO 8601标准来表示时间。 值得庆幸的是,现在有一个名为toISOString
的方法可以用来获取该方法。 -
maxResults
–您要返回的结果总数。 -
singleEvents
–允许您指定是否仅返回单个一次性事件。 在这里,我们使用true
表示重复事件不会被返回。 -
orderBy
–允许您指定返回结果的顺序。 在这种情况下,我们使用了startTime
,它根据结果的开始时间以升序对结果进行排序。 仅当singleEvents
选项设置为true
singleEvents
选项才可用。
所有这些选项以及许多其他选项都可以在“ Events: list
文档中找到。
从数据库获取access_token
并将其用于设置oauth2Client
客户端的凭据。 完成后,创建一个新的cron作业,它将每天在午夜12点运行cache
方法。
connection.db.query('SELECT access_token FROM users WHERE id = 1', function(error, results, fields){
if(!error){
var tokens = JSON.parse(results[0].access_token);
google.oauth2Client.setCredentials({
'access_token': tokens.access_token,
'refresh_token': tokens.refresh_token
});
new CronJob('0 0 * * *', cache, null, true, time.config.timezone);
//cache(); //for testing
}
});
创建通知程序
最后但并非最不重要的一点是,我们有通知程序( notify.js
)。 这负责从数据库中获取约会并确定约会是否成熟。 如果是,那么我们将其发送。
var config = require('config');
var twilio_config = config.get('twilio');
var twilio = require('twilio')(twilio_config.sid, twilio_config.secret);
var connection = require('./common/db');
var time = require('./common/time');
var CronJob = require('cron').CronJob;
function updateAppointment(id){
//update appointment to notified=1
connection.db.query(
"UPDATE appointments SET notified = 1 WHERE id = ?",
[id],
function(error, results, fields){
if(!error){
console.log('updated appointment with ID of ' + id);
}
}
);
}
function sendNotifications(error, results, fields){
var phone_number = config.get('me.phone_number');
console.log(phone_number);
console.log('results');
console.log(results);
if(!error){
for(var x in results){
var id = results[x].id;
var datetime_start = results[x].datetime_start;
var datetime_end = results[x].datetime_end;
var appointment_start = time.moment(datetime_start);
var summary = results[x].summary + " is fast approaching on " + appointment_start.format('MMM DD, YYYY hh:mm a');
var hour_diff = appointment_start.diff(time.moment(), 'hours');
console.log('hour diff:');
console.log(hour_diff);
if(hour_diff <= 24){
twilio.sendMessage({
to: phone_number,
from: twilio_config.phone_number,
body: summary
}, function(err, responseData){
if(!err){
console.log('message sent!');
console.log(responseData.from);
console.log(responseData.body);
}else{
console.log('error:');
console.log(err);
}
});
updateAppointment(id);
}
}
}
}
function startTask(){
connection.db.query('SELECT * FROM appointments WHERE notified = 0', sendNotifications);
}
new CronJob('0 12 * * *', startTask, null, true, time.config.timezone);
分解:
导入所有必需的模块。
var config = require('config');
var twilio_config = config.get('twilio');
var twilio = require('twilio')(twilio_config.sid, twilio_config.secret);
var connection = require('./common/db');
var time = require('./common/time');
var CronJob = require('cron').CronJob;
创建一个updateAppointment
函数。 这接受约会的ID作为其参数。 它所做的全部工作就是将“ notified
字段的值设置为1,这意味着特定约会的通知已经发送。
function updateAppointment(id){
//update appointment to notified=1
connection.db.query(
"UPDATE appointments SET notified = 1 WHERE id = ?",
[id],
function(error, results, fields){
if(!error){
console.log('updated appointment with ID of ' + id);
}
}
);
}
接下来,我们有sendNotifications
函数。 这负责与Twilio实际发送文本提醒。 从数据库中获取约会后,将调用此函数。 这就是为什么将error
, results
和fields
参数传递给它的原因。 该error
包含来自数据库的任何错误。 results
包含从数据库返回的行。 并且这些fields
包含有关返回结果字段的信息。
function sendNotifications(error, results, fields){
var phone_number = config.get('me.phone_number');
console.log(phone_number);
console.log('results');
console.log(results);
if(!error){
for(var x in results){
var id = results[x].id;
var datetime_start = results[x].datetime_start;
var datetime_end = results[x].datetime_end;
var appointment_start = time.moment(datetime_start);
var summary = results[x].summary + " is fast approaching on " + appointment_start.format('MMM DD, YYYY hh:mm a');
var hour_diff = appointment_start.diff(time.moment(), 'hours');
console.log('hour diff:');
console.log(hour_diff);
if(hour_diff <= 24){
twilio.sendMessage({
to: phone_number,
from: twilio_config.phone_number,
body: summary
}, function(err, responseData){
if(!err){
console.log('message sent!');
console.log(responseData.from);
console.log(responseData.body);
updateAppointment(id);
}else{
console.log('error:');
console.log(err);
}
});
}
}
}
}
在该函数内部,我们从应用程序配置中获取用户的电话号码。
var phone_number = config.get('me.phone_number');
console.log(phone_number);
检查是否有任何错误,是否没有错误,然后继续遍历返回的所有结果。
if(!error){
for(var x in results){
...
}
}
在循环内部,我们提取所需的所有值并构造要发送的实际消息。 我们还将获得当前时间与约会开始时间之间的小时差。 我们检查时差是否小于或等于24小时。
var id = results[x].id;
var datetime_start = results[x].datetime_start;
var datetime_end = results[x].datetime_end;
var appointment_start = time.moment(datetime_start);
var summary = results[x].summary + " is fast approaching on " + appointment_start.format('MMM DD, YYYY hh:mm a');
var hour_diff = appointment_start.diff(time.moment(), 'hours');
console.log('hour diff:');
console.log(hour_diff);
if(hour_diff <= 24){
...
}
如果少于或等于24小时,我们将发送通知。 这是通过使用Twilio客户端来完成的。 我们呼吁sendMessage
,并通过在包含对象to
(用户电话号码), from
(Twilio的sandobox号码或电话号码,你从Twilio买),以及body
包含文本消息。 如果没有返回任何错误,则假定通知已发送。 所以我们称之为updateAppointment
功能所设置的notified
字段设置为1,所以它不会在下一次选择的任务运行。
twilio.sendMessage({
to: phone_number,
from: twilio_config.phone_number,
body: summary
}, function(err, responseData){
if(!err){
console.log('message sent!');
console.log(responseData.from);
console.log(responseData.body);
updateAppointment(id);
}else{
console.log('error:');
console.log(err);
}
});
最后,我们有了startTask
方法。 它所做的就是从appointments
表中选择所有尚未发送通知的appointments
。 该函数每中午12点和下午6点执行一次。
function startTask(){
connection.db.query('SELECT * FROM appointments WHERE notified = 0', sendNotifications);
}
new CronJob('0 12,18 * * *', startTask, null, true, time.config.timezone);
结论
而已! 在本教程中,您学习了如何使用Twilio创建SMS提醒应用程序。 具体来说,我们已经研究了如何通过Google Calendar API获取用户的约会。 我们已经将它们保存在数据库中,并通过Twilio通知了用户。 您可以从github repo中找到本教程中使用的代码。
翻译自: https://www.sitepoint.com/build-sms-appointment-reminder-app-with-twilio/
twilio