Many chat apps today implement /slash
commands for their end-users. When done right, these commands can be both practical and engaging, serving a variety of use cases.
For this tutorial, you’ll create a custom /gcal
command that will populate your app's chat channel with your upcoming Google Calendar events and call it with a webhook you'll configure in Stream's dashboard.
As an added bonus, you’ll implement Google’s OAuth 2.0 flow to access Google APIs in your app.
Developer Setup and Prerequisites
This project uses the following stack:
- React.js
- Express
- Node.js
- SQLite
Note: If you don’t know any SQL, that’s ok! By the end of this tutorial, you will be able to write simple SQL queries to execute basic CRUD operations in your server.
In addition to these technologies, you will also need:
- Gmail
- Google Calendar
- Google Cloud Platform
- Ngrok (or another tunneling service)
Before you start: Make sure you've installed Homebrew and the most recent version of Node.js.
To install Homebrew, run the following command:
12$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
You’ll also need npm (version 5.2+) so you can create a project using Create React App (CRA).
With CRA installed, create a new React project:
1$ npx create-react-app google-int-react
Then, create a folder for your server:
12$ mkdir google-int-react-be $ npm init -y
Stream Setup
Next, create a free Stream account. You’ll get a free 30-day trial so you can see what all Stream Chat has to offer.
After creating an account, go to your Stream Dashboard and create a project:
- Select Create App.
- Enter an App Name (like React Google Cal Integration).
- Select your Feeds Server and Data Storage locations.
- Set your Environment to Development.
Go into your project and store your API Key, Secret, and App ID somewhere safe so you can reference them later.
If you want to learn more about integrating Stream chat with your app, check out the React chat SDK tutorial.
Google Cloud Platform Setup
To use Google’s services in your app, you need to create a project on Google Cloud Platform. This requires you to create an account if you haven’t already.
Once you have an account, create a project:
- In your Cloud Console dashboard, go to the Manage Resources page.
- Select Create Project.
- In the New Project window, enter a Project name (like React Google Integration).
- In the Organization dropdown, select No organization.
- Enter the parent organization or folder in the Location box.
- Select Create.
Create a Client ID
This project uses Google Sign-In, which implements the OAuth 2.0 flow to integrate and manage the use of Google APIs in your app.
If you’re unfamiliar with OAuth 2.0, read up on Access Tokens, Refresh Tokens, and Authorization Codes.
To use Google Sign-In, you need a Client ID:
- In your Google Cloud dashboard, click the Navigation menu.
- Go to APIs & Services and select Credentials.
- In the Credentials window, select CREATE CREDENTIALS, then OAuth client ID.
- In the Application type dropdown, select Web application.
- Enter a Name for your Web client ID.
- Under Authorized JavaScript origins, enter the two localhost ports you plan on using for your client and server.
This project uses
localhost:3000
for the client andlocalhost:3001
for the server.
- Select CREATE.
You’re all set! Now, you should see your client ID listed on the Credentials page.
Ngrok Setup
You can use an alternative tunneling service, but the free ngrok plan will suffice for this project.
To get started:
- Go to the ngrok Signup page and enter your Name, E-mail, and Password.
- Click Sign Up.
- In your ngrok dashboard under Getting Started > Setup & Installation, download ngrok for the appropriate OS.
- Then, follow the ngrok installation instructions.
Test your ngrok connection with the following command:
1$ ngrok http 80
If installed correctly, you’ll see a new terminal window labeled ngrok http 80
.
Build Your React Client
With all of your accounts set up, you can organize your project. When bootstrapping a project with CRA, you’ll end up with some unnecessary files, so start by clearing those out.
Under src
, delete the following files:
App.test.js
logo.svg
reportWebVitals.js
setupTests.js
You should also delete any code in App.js
and App.css
.
In index.js
, replace the code with the following:
12345import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root'));
Cd
into src
and create your components
folder:
1$ mkdir components
Then, create your Auth.jsx
and ChannelContainer.jsx
components:
1$ touch Auth.jsx ChannelContainer.jsx
Lastly, create a .env
file in your root project folder:
1$ touch .env
Your project folder structure should look similar to the image below:
With your client set up, now you need to install the following dependencies:
1$ npm i dotenv react-google-login stream-chat stream-chat-react universal-cookie
Set Up Your Client’s Environment Variables
After installing your dependencies, you should create your environment variables.
You’ll need your:
- Stream API Key
- Google Client ID
Place these values into your .env
file:
12REACT_APP_STREAM_API_KEY="XXXXXXXXXXXX" REACT_APP_GOOGLE_CLIENT_ID="XXX.apps.googleusercontent.com"
Initialize Stream Chat
Now, you need to initialize your client with Stream, which requires your API Key.
In App.jsx
, add the following code:
12345678910111213141516171819202122import React from "react"; import { StreamChat } from "stream-chat"; import { Chat } from "stream-chat-react"; require("dotenv").config(); const client = StreamChat.getInstance(process.env.REACT_APP_STREAM_API_KEY); const LogoutButton = ({ logout }) => (<button className="logout-button" onClick={logout}>Logout</button>); const App = () => { const logout = () => {} return ( <div className="app__wrapper"> <Chat client={client}> Chat dashboard </Chat> </div> ); } export default App;
In this snippet, you:
- Pass in your API Key to
StreamChat.getInstance()
to create yourclient
instance. - Wrap your app in the
Chat
context wrapper. - Return your app’s “Chat dashboard”.
All of your components will sit inside Stream’s Chat
context wrapper, providing your app with all the out-of-the-box chat functionality it needs to create a messaging channel.
Build a Simplified Auth Flow
Now, you’ll build a simplified auth flow that uses cookies to set and remove a mock temp_token to log the user in and out of the chat dashboard (later, you’ll generate a unique Stream token in the backend).
In Auth.jsx
, add the following code:
1234567891011121314151617181920import React from 'react'; import Cookies from "universal-cookie"; const cookies = new Cookies(); const tempToken = "temp_token"; const signIn = () => { cookies.set('temp_token', tempToken); window.location.reload(); } const Auth = () => { return ( <div> <button onClick={signIn}>Sign in</button> </div> ) } export default Auth;
In Auth.jsx
, you:
- Instantiate your
cookies
and create atempToken
variable. - Declare your
signIn()
function, which listens for an on-click event to set thetemp_token
and trigger a page reload.
Now, add the following code in App.jsx
:
1234567891011121314151617181920212223242526272829303132333435import React from "react"; import { StreamChat } from "stream-chat"; import { Chat } from "stream-chat-react"; import Auth from "./Auth" import Cookies from "universal-cookie"; const cookies = new Cookies(); const client = StreamChat.getInstance(process.env.REACT_APP_STREAM_API_KEY); const authToken = cookies.get("temp_token"); console.log(authToken) const LogoutButton = ({ logout }) => (<button className="logout-button" onClick={logout}>Logout</button>); const App = () => { const logout = () => { console.log("logout worked") cookies.remove("temp_token"); window.location.reload(); } if (!authToken) return <Auth/>; return ( <div className="App"> <Chat client={client}> Chat dashboard <LogoutButton logout={logout}/> </Chat> </div> ); } export default App;
In the snippet above, you declare a logout()
function that removes your temp_token
when the user clicks the logout
button. It also triggers a page reload.
Now, when a user logs in, the authToken
will trigger a page reload and redirect them to the chat dashboard. Clicking logout
removes the token and triggers a page reload, directing the user to the sign-in page.
Lastly, add the following code to your App.css
file:
12345678body { text-align: center; padding-top: 15%; } button { margin-left: 10px; }
You should be able to sign in and out of your app:
Building Your Backend
With your basic front-end structure out of the way, you can build your backend:
Open your backend project. In your project folder, create the following folders and files with the commands below:
12$ mkdir controllers database models routes $ touch .env index.js
Your project structure should look similar to the one below:
To get your project running, install the following dependencies:
1$ npm i cors crypto dotenv express getstream google-auth-library googleapis nodemon sqlite3 stream-chat
With all your dependencies installed, configure your script in package.json
so you can easily run nodemon to automatically restart your server after you save changes:
1234"scripts": { "start": "node index.js", "dev": "nodemon index.js" }
Now, go to index.js
and add the following code:
1234567891011121314151617181920212223242526272829303132const express = require("express"); const bodyParser = require("body-parser"); const cors = require("cors"); // const authRoutes = require("./routes/auth"); const app = express(); app.use(bodyParser.json({ verify: (req, res, buf) => { req.rawBody = buf; } })); app.use((request, response, next) => { next(); }); const PORT = "3001"; require('dotenv').config(); app.use(cors()); app.use(express.json()); app.use(express.urlencoded()); app.get('/', (req, res) => { res.send("hello world"); }); // app.use('/auth', authRoutes); app.listen(PORT, () => { console.log(
Now, run npm run dev
in your terminal. Your server should greet you with a “hello world” on your localhost port in your browser 😎
Define Your Environment Variables
In your .env
file, you’ll need your Stream and Google secrets so you can use them throughout your server.
You can find all the required values in your Stream dashboard and under your OAuth 2.0 Client IDs in the Credentials section of your Google Cloud Platform dashboard.
Once you’ve entered those values, your .env
file should look similar to the file below:
123456STREAM_API_KEY="XXXXXXXX" STREAM_SECRET="XXXXXXXX" STREAM_ID="XXXXXXXX" GOOGLE_CLIENT_ID="XXX.apps.googleusercontent.com" GOOGLE_CLIENT_SECRET="XXXXX" GOOGLE_REDIRECT_URL="http://localhost:3000"
Define Your Routes and Controllers
In your ./routes
folder, create a new file called auth.js
:
1$ touch auth.js
In routes/auth.js
, enter the following code:
1234567891011const express = require('express'); const { login, googleauth, handlewebhook } = require("../controllers/auth"); const router = express.Router(); router.post('/login', login); router.post('/googleauth', googleauth); router.post('/handlewebhook', handlewebhook) module.exports = router;
In the snippet above, you direct your server’s routes to the specified endpoints. But if you fire up your server now, it should throw an error. That’s because you haven’t defined these endpoints in ../controllers/auth
yet.
Back in your controllers
folder, create another auth.js
file:
1$ touch auth.js
Now, go to controllers/auth.js
and define the handlers you’ll need for your backend with the following code:
1234require('dotenv').config(); const login = async (req, res) => {} const googleauth = async (req, res) => {}
Note: Later, you’ll define setupCommands()
and handlewebhook()
functions to handle your webhook. For now, you just need login()
and googleauth()
.
Before continuing, you’ll need some way to persist your user’s Stream data, which calls for a database.
Implement Your SQLite Database
SQLite is a lightweight SQL database engine that’s perfect for the scope of this project.
To get started, you’ll need to install SQLite. If you use a Mac, it should be installed already. You can check by entering sqlite3
into your terminal. If it’s installed, you’ll see the following response in your terminal:
If it’s not installed on your machine, go to the SQLite download page and follow the instructions.
Once installed, enter your database
folder and create the following files:
db.js
: This is where you’ll configure your database connectiondb.sql
: This is where you’ll create yourusers
tableproj.db
: This is a simple text file that will contain your project’s in-memory database
In database
, run the following command:
1$ touch db.js db.sql proj.db
In db.js
, enter the following code:
1234567const sqlite3 = require("sqlite3").verbose(); const db = new sqlite3.Database('database/proj.db', sqlite3.OPEN_READWRITE, (err) => { if (err) return console.error(err.message); console.log("connection to db successful"); }); module.exports = db;
If you fire up your server, you should see “connection to db successful” in your console.
Now, you're ready to create your users
table.
In db.sql
, enter the following code:
123456CREATE TABLE IF NOT EXISTS users ( name TEXT DEFAULT "", email TEXT DEFAULT "", user_id TEXT DEFAULT "", refresh_token TEXT DEFAULT "" );
This is a basic SQL command that tells your database to create a table if it doesn't exist. In this table, you specify what columns you need and what data types your DB should expect when you insert data.
This is a straitforward DB schema, where the data will be of the type TEXT
, which means you can insert your data as a String.
Enter the following command to create your table:
12$ sqlite3 database/proj.db $ .read database/db.sql
To see if you created your users
table successfully, enter .tables
in your terminal. You should see your table listed:
With your database up and running, you can define some basic CRUD operations for your app.
Go to your models
folder and create a file called data-models.js
:
1$ touch data-models.js
In data-models.js
, enter the following code:
12345const db = require("../database/db"); function findUser(email) { return new Promise((resolve, reject) => { const getUser =
In this code block, you define and export several functions that you’ll use to interact with your database:
findUser(email)
: Checks if a user exists in your database with theiremail
.insertUsers(name, email, user_id)
: Creates a new user by inserting theirname
,email
, anduser_id
into your database’susers
table.updateRefreshToken(refresh_token, email)
: Gives the user a newrefresh_token
.getRefreshToken(email)
: Retrieves the user’s refresh token with theiremail
.getRefreshTokenWithId(user_id)
: Retrieves the user’s refresh token with theiruser_id
.
That’s it for your database! Now, back to your endpoints.
Backend Auth Flow With Stream and Google
Now, you can set up your Stream and Google auth flow.
Back in controllers/auth.js
, replace your code with the following:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475const {StreamChat} = require('stream-chat'); const crypto = require('crypto'); const dataModel = require("../models/data-model"); require('dotenv').config(); // Google Cal integration const {google} = require('googleapis'); const { OAuth2Client } = require('google-auth-library'); const scopes = ['https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.events']; // Set Google and Stream environment variables const googleClientId = process.env.GOOGLE_CLIENT_ID; const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET; const googleRedirectUrl = process.env.GOOGLE_REDIRECT_URL; const api_key = process.env.STREAM_API_KEY; const api_secret = process.env.STREAM_SECRET; const app_id = process.env.STREAM_ID; // Instantiate Google OAuth 2.0 client const oAuth2Client = new google.auth.OAuth2( googleClientId, googleClientSecret, googleRedirectUrl ); // set auth as a global default google.options({ auth: oAuth2Client }); const login = async (req, res) => { try { // Google Auth const googleToken = req.body.token; const ticket = oAuth2Client.verifyIdToken({ idToken: googleToken, audience: process.env.GOOGLE_CLIENT_ID }); const {name, email} = (await ticket).getPayload() const url = oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: scopes }); // Stream Auth const serverClient = StreamChat.getInstance(api_key, api_secret, app_id); // await setupCommands(serverClient); // SQL Queries -- Find user if exists; otherwise, insert new user const userFound = await dataModel.findUser(email); if(!userFound) { const user_id = crypto.randomBytes(16).toString('hex'); dataModel.insertUsers(name, email, user_id).then((newUser) => { console.log(newUser) }); const token = serverClient.createToken(user_id); return res.status(200).json({token, user_id, name, email, url}) } // Pass in user_id and generate Stream token const user_id = userFound.user_id; const token = serverClient.createToken(user_id); res.status(200).json({token, user_id, name, email, url}) } catch (error) { console.log(error); res.status(500).json({message: error}) } }
Note what’s happening in this code block:
- You import
googleapis
and theOAuth2Client
library. - You declare your
scopes
, which will prompt the user to give their consent for you to access their Google Calendar data. - You declare your Stream and Google
.env
variables. - You instantiate your Google OAuth 2.0 (
oAuth2Client
) client using thegoogle.auth.OAuth2
constructor.
Note: The
MARKDOWN_HASH957498a5940da078121d2fc7f984da47MARKDOWNHASH
instance is important, as it keeps track of your refresh and access_tokens for you. You'll use it in multiple places throughout your server.
Here’s what’s happening in login
:
- You handle your Google token and profile object (you’ll set this up in your client soon), which generates an authorization
url
. Thisurl
is necessary to generate the consent form for your users, and specifies your permission scopes. - You initialize your server-side Stream client with your
.env
variables. - Your
dataModel.findUser()
function checks if there’s a user with the same email in the database. If no user exists, you generate a unique Streamuser_id
and callinsertUsers
to create the user in the database and generate their Stream token. - If the user does exist, you pass in the
user_id
to create a new Stream token. - Finally, you send your
token
,user_id
,name
,email
, andurl
back to your front-end client.
With this data, you can authenticate a user on your front-end with a Stream token, replacing the mock token you used earlier. You can also prompt the user to give you access to their Google Calendar data.
Integrating Google Sign-In
Back in your client, you’ll add React Google Login to handle your Google sign-in.
In Auth.jsx
, replace the file with the following code:
1234567891011121314import React from 'react'; import Cookies from "universal-cookie"; import GoogleLogin from "react-google-login"; require("dotenv").config(); const cookies = new Cookies(); const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID; const Auth = () => { const responseGoogle = (googleRes) => { const data = googleRes.profileObj; console.log(
In this snippet:
- You place the
GoogleLogin
component in yourreturn
statement. - You pass in your
clientId
as a prop and theresponseGoogle
function to handle Google’s response when you sign in.- In
responseGoogle
, you’re expecting a profile object, which includes your name, email, and other data related to your Google account.
- In
- Next, you specify the
URL
endpoint you want to hit in your server and create yourfetch
request, which must include thedata
andtokenId
from the profile object. - Upon a successful request, you’ll receive your
name
,email
, Streamtoken
, Streamuser_id
, and Google’s unique authorizationurl
. You’ll store thename
,email
,token
, anduser_id
in cookies withcookies.set()
. - Lastly, you use
window.location.assign(url)
to navigate to Google's authorizationurl
, which contains acode
parameter you’ll pass to your server assuming the user grants access to the requested permission scopes.
To handle the code
parameter and log your user into their chat channel, you need to update App.jsx
with the following code:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849import React from "react"; import { StreamChat } from "stream-chat"; import { Chat } from "stream-chat-react"; import "./App.css" import Cookies from "universal-cookie"; import Auth from "./components/Auth" import ChannelContainer from "./components/ChannelContainer"; require("dotenv").config(); const client = StreamChat.getInstance(process.env.REACT_APP_STREAM_API_KEY); const cookies = new Cookies(); const authToken = cookies.get('token') if (authToken) { client.connectUser({ id: cookies.get('user_id'), name: cookies.get("name"), }, authToken); } const LogoutButton = ({ logout }) => (<button className="logout-button" onClick={logout}>Logout</button>); const App = () => { const logout = () => { cookies.remove('token'); cookies.remove('name'); cookies.remove('user_id'); cookies.remove('code'); window.location.reload(); } if (!authToken) return <Auth /> return ( <div className="app__wrapper"> <Chat client={client}> <div className="sidebar"> <LogoutButton logout={logout} /> </div> <div> <ChannelContainer /> </div> </Chat> </div> ); } export default App;
In this snippet:
- You retrieve your Stream
token
withcookies.get(‘token’)
and declare theauthToken
variable. - Then, with
connectUser()
, you pass in theuser_id
,name
, andauthToken
to create a websocket connection with theclient
. - You pass in the
client
object as a prop to yourChat
context, which you’ll use in yourChannelContainer
component to create a channel. - When the user clicks
logout
, you remove your cookies and close out the session. - Because you have an
authToken
, your app redirects the user to the chat dashboard insideChannelContainer
.
At this point, Google will redirect the user to the authorization url that you received in Auth.jsx
with the scopes you defined in your server:
Here, when your user grants permission to the scopes, they'll be redirected to your chat dashboard. If they deny access, they'll be sent back to the sign-in page.
The last component you need to handle is in ChannelContainer.jsx
.
Add in the following code:
123456789101112131415import React, { useEffect } from 'react'; import { Channel, Window, ChannelHeader, MessageList, MessageInput, Thread, useChatContext } from 'stream-chat-react'; import Cookies from 'universal-cookie'; import '../App.css'; import 'stream-chat-react/dist/css/index.css'; const cookies = new Cookies(); const ChannelContainer = () => { useEffect(() => { const params = new URLSearchParams(window.location.search.substring(1)); let code = params.get("code"); cookies.set("code", code); const URL =
In this snippet:
- You import the
stream-chat-react
css file to give your channel a cleaner layout. - You use
useChatContext()
to access theclient
object, where you get your user’sid
to create achannel
. - Then, you pass
channel
as a prop into theChannel
component. - In your
useEffect
hook, you parse thecode
parameter from theurl
your user lands on. - In your
fetch
request, you send youremail
andcode
to the/auth/googleauth
endpoint.
To add a little bit more customization to your app’s look and feel (and to override some of Stream’s default css properties), add the following code to your App.css
file:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182* { box-sizing: border-box; text-align: center; } html, body { margin: 0; padding: 0; height: 100%; } .app__wrapper { display: flex; } /* Hooks button */ .button { cursor: pointer; display: block; font-size: 1.3em; box-sizing: content-box; margin: 20px auto 0px; width: 70%; padding: 15px 20px; border-radius: 24px; border-color: transparent; background-color: white; box-shadow: 0px 16px 60px rgba(78, 79, 114, 0.08); position: relative; } .sidebar { padding: 20px; width: 15%; --tw-bg-opacity: 1; background-color: rgba(0,95,255,var(--tw-bg-opacity)); } .logout-button { height: 35px; border-radius: 20px; border: none; width: 135px; margin-top: 150px; --tw-bg-opacity: 1; background-color: white; color: black; cursor: pointer; font-weight: 700; } .buttonText { color: #4285f4; font-weight: bolder; } .icon { height: 25px; width: 25px; margin-right: 0px; position: absolute; left: 30px; align-items: center; } div { box-sizing: border-box; text-align: center; width: 100%; } .str-chat__input-flat .rfu-file-upload-button { width: 1%; } span { white-space: nowrap; } li { text-align: left; }
Handling Google’s Refresh and Access Tokens
A core part of OAuth 2.0 is refresh and access tokens. When using Google’s OAuth 2.0 flow, access tokens are required to make requests to Google APIs. This includes Google Calendar.
But, access tokens expire quickly, which means you need to generate a new one with your refresh token. Google handles this process automatically for you, but it’s on you as the developer to store your refresh token.
This next section will show you how:
In your controllers
folder in auth.js
, enter the following code:
1234567891011121314151617181920212223242526const googleauth = async (req, res) => { try { const { email } = req.body; const {code} = req.body; const tokenRes = await dataModel.getRefreshToken(email); if(!tokenRes.refresh_token) { const r = await oAuth2Client.getToken(code); oAuth2Client.on('tokens', async (tokens) => { // On first authorization, store refresh_token if (tokens.refresh_token) { const refresh_token = tokens.refresh_token; dataModel.updateRefreshToken(refresh_token, email) } }); oAuth2Client.setCredentials(r.tokens); return res.status(200).json({message: "User authorized"}) } res.status(200).json({message: "User authorized"}); } catch (error) { console.log(error); } }
In this snippet, you receive the authorization code
and email
from your front-end, and then check if you’ve previously stored a refresh_token
with dataModel.getRefreshToken(email)
.
If you have a refresh token, your server does nothing with the code. If you don’t have a refresh token, you pass in the code
to oAuth2Client.getToken()
, which will generate an access_token
and refresh_token
, but only on the first authorization.
On subsequent authorizations, Google will look for your stored refresh token to exchange for a new access token. To manage your tokens, use Google’s oAuth2Client.on()
token event, which stores your refresh and access tokens for you (you still have to manually set your refresh token to get a new access token, but you’ll cover that in your webhook configuration).
After receiving your tokens, call setCredentials
to authorize your user.
Configuring Your Webhook
Finally, now you can create a custom webhook.
First, fire up an ngrok process for your server:
1$ ngrok https 3001
In your Stream dashboard:
- Click on your app.
- In the nav, select Chat, then Overview.
- Under the Realtime window, specify your url.
- Note: This must be a publicly accessible URL. In addition to generating your URL with ngrok, you also need to add the /auth/handlewebhook endpoint.
- Select Save.
Now, in components/auth.js
, add the following line of code to login
under your serverClient
instance:
1await setupCommands(serverClient);
Then, create your setupCommands
function to configure your custom commands webhook:
12345// Create webhook const setupCommands = async (serverClient) => { try { const ngrokUrl =
In this snippet, you’re using your ngrokUrl
for your custom_action_handler_url
, and setting up the command so that Stream knows to look for the /gcal
custom command.
Lastly, create your handlewebhook
handler:
12345678910111213141516171819202122232425262728293031323334353637383940414243const handlewebhook = async (req, res) => { const { user, message, form_data } = req.body; const user_id = user.id; const rToken = await dataModel.getRefreshTokenWithId(user_id).then((data) => { return data }); oAuth2Client.setCredentials({refresh_token: rToken.refresh_token}); const calendar = google.calendar({version: 'v3', oAuth2Client}); const response = await calendar.events.list({ calendarId: 'primary', timeMin: (new Date()).toISOString(), maxResults: 5, singleEvents: true, timeMin: (new Date()).toISOString(), orderBy: 'startTime', }); const r = await response; const events = r.data.items; const list = events.map(event => { const hStart = event.start.dateTime.substr(11, 2); const hEnd = event.end.dateTime.substr(11, 2); const mStart = event.start.dateTime.substr(14, 2); const mEnd = event.end.dateTime.substr(14, 2); const hStartNum = parseInt(hStart); const hEndNum = parseInt(hEnd); const hoursStart = ((hStartNum + 11) % 12 + 1); const hoursEnd = ((hEndNum + 11) % 12 + 1); hoursStart.toString(); hoursEnd.toString(); const startString = hoursStart + ":" + mStart + " "; const endString = hoursEnd + ":" + mEnd + " "; const summary = event.summary.trim(); const eventString =
In this snippet:
- You get your
user_id
and themessage
(/gcal
) from your request and fetch your user’s refresh token withgetRefreshTokenWithId()
. - Using your global
oAuthClient2
instance, you callsetCredentials({refresh_token: rToken.refresh_token})
to get a new access token.- Note: This part is crucial. Google will automatically replace your expired access token with a new one, but you must call
setCredentials
with the{refresh_token}
object here to do so. If you don’t, your access token will expire, which will force the user to log in again to get new access and refresh tokens.
- Note: This part is crucial. Google will automatically replace your expired access token with a new one, but you must call
- Next, you call
google.calendar()
and pass in youroAuth2Client
to make a call to the Google Calendar API to retrieve yourevents
.
You’ll get a list of the next five events
from your Google Calendar. Using some JavaScript, you can then extract start
and end
times from your Date
strings.
Lastly, using Stream’s Message Markup Language (MML), which is currently only supported in the React SDK, you can create a list of your events using MarkDown and send it back to your frontend client.
The end result will look like this:
Wrapping Up
Congratulations! In this tutorial, you successfully implemented Google’s OAuth 2.0 flow using refresh and access tokens to make API calls to retrieve events from Google Calendar.
You also configured your own webhooks in your server and in Stream’s dashboard, enabling you to implement your own custom commands.
Plus, you built the beginnings of a full-stack application that uses a SQL database and some basic SQL queries.
You can find all the code for the front-end project in this GitHub repo, and the code for the backend project in this GitHub repo.
As always, show us what you're working on @getstream_io and keep coding!