ทดลองดึงข้อมูล Calendar ด้วย Google Calendar API บน Nodejs

Published on
NodeJS
2016/05/create-google-calendar-api-with-nodejs
Discord

พอดีได้ลองเล่นตัว Google Calendar API ก็เลยนำมาเขียนเป็นบทความแชร์ไว้ ซึ่ง Tutorial ทั้งหมดผมก็อ่านจาก Google Calendar API Quickstart ของมันแหละครับ ซึ่งข้อมูลอย่างละเอียดแนะนำอ่าน Guides,Quickstart เพิ่มเติมเอานะครับ ส่วน Source Code ทั้งหมดดูได้บน Github

Step 1 : Create Project

เริ่มต้นสร้างโปรเจ็ค ผมทำการสร้างโปรเจ็ค ด้วย

npm init

จากนั้นก็ทำการติดตั้ง dependencies ของ google

npm install googleapis googlepauth-library --save

ต่อมาผมติดตั้ง hapi ซึ่งเอาไว้รัน web serve (หรือใครถนัดใช้ express ก็เปลี่ยนได้ครับ)

npm install hapi --save

และทำการสร้าง server ง่ายๆขึ้นมาตัวนึง ชื่อไฟล์ server.js

'use strict'

const Hapi = require('hapi')
const server = new Hapi.Server()

server.connection({
  host: 'localhost',
  port: 2345
})

server.route({
  method: 'GET',
  path: '/',
  handler: (request, reply) => {
    reply({ message: 'Hello World' })
  }
})

server.start(() => {
  console.log(`Server is running at ${server.info.uri}`)
})

เพิ่ม scripts เข้าไปในไฟล์ package.json

"scripts": {
  "start": "node server.js"
}

แล้วสั่งรัน

npm start

จะได้ server ที่รันอยู่บน port 2345 แล้ว

Step 2 : Enable Calendar API

ต่อมาก่อนที่เราจะใช้งาน Calendar API นั้น ให้ทำการ enable ตัว Google Calendar API ก่อน เพื่อให้มีสิทธิ์ในการเรียกใช้งาน API โดยเข้าไปที่ Enable Google Calendar API (เลือก Project ที่เราต้องการ หรือไม่ก็สร้างขึ้นมาใหม่)

จากนั้นเลือกแท็ป Credentials ซ้ายมือ แล้วเลือก Oauth consent screen จากนั้นกรอก email และ Product Name

จากนั้นเลือก Credentials => Create credentials => OAuth client ID เลือก Others และตั้งชื่อตามที่ต้องการ

เมื่อสร้างเสร็จ ก็จะได้ไฟล์ JSON ทำการดาวน์โหลดมามาไว้ในโปรเจ็คของเรา ตั้งชื่อว่า client_secret.json

ตัวไฟล์ client_secret.json จะมีข้อมูลของแอพเราอยู่ประมาณนี้

{
  "installed": {
    "client_id": "xxxxxxx.apps.googleusercontent.com",
    "project_id": "project_id_1234",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://accounts.google.com/o/oauth2/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "client_secret123456",
    "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
  }
}

Step 3 : Run quickstart

ทำการดาวน์โหลดไฟล์จาก quickstart.js

var fs = require('fs')
var readline = require('readline')
var google = require('googleapis')
var googleAuth = require('google-auth-library')

// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/calendar-nodejs-quickstart.json
var SCOPES = ['https://www.googleapis.com/auth/calendar']
var TOKEN_DIR =
  (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) +
  '/.credentials/'
var TOKEN_PATH = TOKEN_DIR + 'calendar-nodejs-quickstart.json'

// Load client secrets from a local file.
fs.readFile('client_secret.json', function processClientSecrets(err, content) {
  if (err) {
    console.log('Error loading client secret file: ' + err)
    return
  }
  // Authorize a client with the loaded credentials, then call the
  // Google Calendar API.
  authorize(JSON.parse(content), listEvents)
})

/**
 * Create an OAuth2 client with the given credentials, and then execute the
 * given callback function.
 *
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback to call with the authorized client.
 */
function authorize(credentials, callback) {
  var clientSecret = credentials.installed.client_secret
  var clientId = credentials.installed.client_id
  var redirectUrl = credentials.installed.redirect_uris[0]
  var auth = new googleAuth()
  var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl)

  // Check if we have previously stored a token.
  fs.readFile(TOKEN_PATH, function (err, token) {
    if (err) {
      getNewToken(oauth2Client, callback)
    } else {
      oauth2Client.credentials = JSON.parse(token)
      callback(oauth2Client)
    }
  })
}

/**
 * Get and store new token after prompting for user authorization, and then
 * execute the given callback with the authorized OAuth2 client.
 *
 * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
 * @param {getEventsCallback} callback The callback to call with the authorized
 *     client.
 */
function getNewToken(oauth2Client, callback) {
  var authUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES
  })
  console.log('Authorize this app by visiting this url: ', authUrl)
  var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  })
  rl.question('Enter the code from that page here: ', function (code) {
    rl.close()
    oauth2Client.getToken(code, function (err, token) {
      if (err) {
        console.log('Error while trying to retrieve access token', err)
        return
      }
      oauth2Client.credentials = token
      storeToken(token)
      callback(oauth2Client)
    })
  })
}

/**
 * Store token to disk be used in later program executions.
 *
 * @param {Object} token The token to store to disk.
 */
function storeToken(token) {
  try {
    fs.mkdirSync(TOKEN_DIR)
  } catch (err) {
    if (err.code != 'EEXIST') {
      throw err
    }
  }
  fs.writeFile(TOKEN_PATH, JSON.stringify(token))
  console.log('Token stored to ' + TOKEN_PATH)
}

/**
 * Lists the next 10 events on the user's primary calendar.
 *
 * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
 */
function listEvents(auth) {
  var calendar = google.calendar('v3')
  calendar.events.list(
    {
      auth: auth,
      calendarId: 'primary',
      timeMin: new Date().toISOString(),
      maxResults: 10,
      singleEvents: true,
      orderBy: 'startTime'
    },
    function (err, response) {
      if (err) {
        console.log('The API returned an error: ' + err)
        return
      }
      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 start = event.start.dateTime || event.start.date
          console.log('%s - %s', start, event.summary)
        }
      }
    }
  )
}

โดยได้ทำการเปลี่ยนแค่ SCOPES จาก

var SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']

เป็น

var SCOPES = ['https://www.googleapis.com/auth/calendar']

สำหรับเพิ่ม permission เอาไว้จัดการ เพิ่มหรือลบ event ได้ ทดลองสั่งรัน

node quickstart.js
Authorize this app by visiting this url:  https://accounts.google.com/o/oauth2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly&response_type=code&client_id=12234.apps.googleusercontent.com&redirect_uri=xxxxx
Enter the code from that page here:

จะได้ output ออกมา (ไม่เหมือนกัน ขึ้นอยู่กับชื่อ project, clientId) ให้ก็อปไปวางไว้บน browser แล้วก็ทำการ Authorized จากนั้นก็ใส่ code กลับมาที่ terminal อีกรอบ

ไฟล์ autrorized จะถูกเซฟไว้ที่ ~/.credentials/calendar-nodejs-quickstart.json

ซึ่งมาถึงตรงนี้ หากใครแสดงรายชื่อ events บน Calendar ได้แล้ว แสดงว่าสามารถเรียกใช้ API ได้ เป็นอันจบ

แต่....เดี๋ยวก่อน ผมไม่อยากใช้การ รัน node quickstart.js แบบ Tutorial ก็เลยทำเป็น library ซะเลย

Step 4 : Create library

ขั้นตอนนี้ผมจะนำไฟล์ quickstart.js มาแปลงเป็น library เพื่อให้เรียกใช้งานง่ายๆผ่าน Server hapi พร้อมทั้งเพิ่มการสร้าง event เข้าไปใน Calendar ได้

สร้างไฟล์ lib/index.js ขึ้นมา ในไฟล์ library ผมแปลงจาก quickstart.js เป็น library แบบนี้

'use strict'

const google = require('googleapis')
const googleAuth = require('google-auth-library')
const calendar = google.calendar('v3')

const fs = require('fs')

const SCOPES = [process.env.SCOPES]
const TOKEN_DIR =
  (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) +
  '/.credentials/'
const TOKEN_PATH = TOKEN_DIR + 'calendar-nodejs-quickstart.json'

module.exports = {
  authorize: callback => {
    fs.readFile('client_secret.json', function processClientSecrets(
      err,
      content
    ) {
      if (err) {
        console.log('Error loading client secret file: ' + err)
        return
      }

      let credentials = JSON.parse(content)
      var clientSecret = credentials.installed.client_secret
      var clientId = credentials.installed.client_id
      var redirectUrl = credentials.installed.redirect_uris[0]
      var auth = new googleAuth()
      var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl)

      // Check if we have previously stored a token.
      fs.readFile(TOKEN_PATH, function (err, token) {
        if (err) {
          return callback(err)
        } else {
          oauth2Client.credentials = JSON.parse(token)
          return callback(null, oauth2Client)
        }
      })
    })
  },

  getNewToken: (oauth2Client, callback) => {
    var authUrl = oauth2Client.generateAuthUrl({
      access_type: 'offline',
      scope: SCOPES
    })
    console.log('Authorize this app by visiting this url: ', authUrl)
    var rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    })
    rl.question('Enter the code from that page here: ', function (code) {
      rl.close()
      oauth2Client.getToken(code, function (err, token) {
        if (err) {
          console.log('Error while trying to retrieve access token', err)
          return
        }
        oauth2Client.credentials = token
        storeToken(token)
        callback(oauth2Client)
      })
    })
  },

  listEvents: (auth, callback) => {
    calendar.events.list(
      {
        auth: auth,
        calendarId: process.env.CALENDAR_ID,
        timeMin: new Date().toISOString(),
        maxResults: 50,
        singleEvents: true,
        orderBy: 'startTime'
      },
      (err, response) => {
        if (err) {
          return callback(err)
        }
        return callback(null, response)
      }
    )
  },

  createEvent: (auth, event, callback) => {
    calendar.events.insert(
      {
        auth: auth,
        calendarId: process.env.CALENDAR_ID,
        resource: event
      },
      (err, event) => {
        if (err) {
          return callback(
            'There was an error contacting the Calendar service: ' + err
          )
        }
        return callback(null, event.htmlLink)
      }
    )
  }
}

ซึ่งเวลาเรียกใช้งานผ่านทาง express ผมก็แค่

const lib = require('./lib')

lib.authorize()
lib.listEvents()
lib.createEvent()

แต่ว่าทำเป็น callback ไม่ได้ใช้ Promise ซึ่งเวลาเรียกใช้งานจำเป็นที่จะต้องส่ง Callback ไปด้วย

Step 5 : Implement Server Side

ต่อมาในส่วน hapi server ผมอยากใช้มี route 2 อัน คือ

  • GET /events : สำหรับแสดง events จาก google calendar
  • POST /events : สำหรับสร้าง events แบบเดียวกับสร้างในหน้า Calendar

แน่นอน ทั้งหมดผมทำเป็น API ไม่ได้มีหน้า UI ฉะนั้นการรับส่งข้อมูลก็จะเป็นแค่ JSON นะครับ

สร้างไฟล์ routes.js ขึ้นมาดังนี้

'use strict'

const controller = require('./controller')

module.exports = [
  {
    method: 'GET',
    path: '/',
    config: controller.index
  },
  {
    method: 'GET',
    path: '/events',
    config: controller.events
  },
  {
    method: 'POST',
    path: '/events',
    config: controller.create
  }
]

สำหรับ hapi js เล็กน้อย ในเรื่อง routing เราสามารถกำหนดได้ทั้ง handler: function(request, reply) หรือจะเป็น config: Object ก็ได้ครับ รายละเอียดเพิ่มเติม http://hapijs.com/tutorials/routing

ต่อมาผมสร้างไฟล์ controller.js ขึ้นมา

'use strict'

const validate = require('./validate')
const lib = require('./lib')

module.exports = {
  index: {
    handler: (request, reply) => {
      reply({
        message: 'Google Calendar API',
        endpoint: {
          listEvents: 'GET /events',
          createEvent: 'POST /events'
        }
      })
    }
  },

  create: {
    validate: validate.create,
    handler: (request, reply) => {
      let payload = request.payload

      let summary = payload.summary
      let description = payload.description
      let email = payload.email
      let startDate = payload.startDate
      let endDate = payload.endDate

      lib.authorize((err, auth) => {
        if (err) return reply(err)

        let options = lib.eventBuilder(payload)
        options.auth = auth

        lib.createEvent(options, (err, result) => {
          if (err) return reply(err)

          return reply(result)
        })
      })
    }
  },

  events: {
    handler: (request, reply) => {
      lib.authorize((err, auth) => {
        if (err) return reply(err)

        lib.listEvents(auth, (err, response) => {
          if (err) return reply(err)

          return reply(response)
        })
      })
    }
  }
}

ผมเพิ่ม validate.js เล็กน้อย เวลาที่ส่ง parameters สำหรับ POST method จะได้ไม่มีปัญหา

'use strict'

const Joi = require('joi')

module.exports = {
  create: {
    payload: {
      email: Joi.string().email().required(),
      title: Joi.string().required(),
      description: Joi.string().required(),
      startDate: Joi.string().required(),
      endDate: Joi.string().required()
    }
  }
}

สุดท้ายไฟล์ server.js

'use strict'

const Hapi = require('hapi')
const server = new Hapi.Server()
const routes = require('./routes')

server.connection({
  host: 'localhost',
  port: 2345
})

server.route(routes)

server.start(() => {
  console.log(`Server is running at ${server.info.uri}`)
})

ทดลองสั่งรัน server

node server.js

เข้าหน้าเว็บ http://localhost:2345/ เป็นอันเรียบร้อย

สรุป

ตัวอย่างทั้งหมดอันนี้ผมก็ศึกษาจาก Google Calendar API และลองทำเล่นๆเท่านั้น จะเห็นว่าไม่มีหน้า UI เนื่องจากผมกะเอาไปใช้งานร่วมกับคำสั่งพวก slash command ของ Slack หรือว่า bot service อื่นๆ ดูในอนาคตครับ

สุดท้าย Source Code ครับ

Buy Me A Coffee
Authors
Discord