Skip to content

Commit d99bfe1

Browse files
feat: add connectivity chat app in Apps Script (#209)
1 parent 321dcaa commit d99bfe1

7 files changed

Lines changed: 276 additions & 4 deletions

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const APP_NAME = 'Connectivity app';
18+
const LOGOUT_COMMAND_ID = 1;
19+
20+
// Set to the client_secrets.json file content
21+
const CLIENT_SECRETS = {};
22+
23+
/**
24+
* Responds to a MESSAGE event in Google Chat.
25+
*
26+
* @param {Object} event the event object from Google Workspace Add On
27+
*/
28+
function onMessage(event) {
29+
try {
30+
// Try to obtain an existing OAuth2 token from storage.
31+
var meetService = getMeetService_();
32+
if (!meetService.hasAccess()) {
33+
// App doesn't have credentials for the user yet.
34+
// Request configuration to obtain OAuth2 credentials.
35+
getConfigRequestResponse(meetService).throwException();
36+
}
37+
38+
// Call Meet API to create the new space with the user's OAuth2 credentials.
39+
var response = UrlFetchApp.fetch('https://meet.googleapis.com/v2/spaces', {
40+
method: 'post',
41+
contentType: 'application/json',
42+
headers: { Authorization: 'Bearer ' + meetService.getAccessToken() },
43+
// An empty body is sufficient to create a default space.
44+
payload: JSON.stringify({})
45+
});
46+
var meetSpace = JSON.parse(response.getContentText());
47+
return sendCreateTextMessageAction(`New Meet was created: ${meetSpace.meetingUri}`);
48+
} catch (e) {
49+
// This error probably happened because the user revoked the
50+
// authorization. So, let's request configuration again.
51+
Logger.log('Error creating Meet space: ' + JSON.stringify(e));
52+
throw e;
53+
}
54+
}
55+
56+
/**
57+
* Responds to a APP_COMMAND event in Google Chat.
58+
*
59+
* @param {Object} event the event object from Google Workspace Add On
60+
*/
61+
function onAppCommand(event) {
62+
// Extract data from the event.
63+
const chatEvent = event.chat;
64+
const appCommandId = chatEvent.appCommandPayload.appCommandMetadata.appCommandId;
65+
66+
if (appCommandId == LOGOUT_COMMAND_ID) {
67+
// Delete OAuth2 credentials from storage if any.
68+
resetMeetServiceAuth();
69+
// Reply a Chat message with confirmation
70+
return sendCreateTextMessageAction('You are now logged out!');
71+
}
72+
}
73+
74+
// ----------------------
75+
// Chat utils
76+
// ----------------------
77+
78+
/**
79+
* Returns an action response that tells Chat to reply with a text message.
80+
*
81+
* @param {!string} text The text of the message to reply with.
82+
* @returns {Object} An ActionResponse message.
83+
*/
84+
function sendCreateTextMessageAction(text) {
85+
return { hostAppDataAction: { chatDataAction: { createMessageAction: { message: { text: text } }}}};
86+
}
87+
88+
/**
89+
* Returns an action response that tells Chat to request configuration for the
90+
* app. The configuration will be tied to the user who sent the event.
91+
*
92+
* @param {!Service_} meetService The Meet API OAuth2 service
93+
* @param {!string} configCompleteRedirectUrl The URL to redirect to after
94+
* completing the flow.
95+
* @return {Object} An ActionResponse message request additional configuration.
96+
*/
97+
function getConfigRequestResponse(meetService) {
98+
return CardService.newAuthorizationException()
99+
.setAuthorizationUrl(meetService.getAuthorizationUrl())
100+
.setResourceDisplayName(APP_NAME);
101+
}
102+
103+
// ----------------------
104+
// OAuth2 utils
105+
// ----------------------
106+
107+
/**
108+
* Create a new OAuth2 service to facilitate accessing the Meet API.
109+
*
110+
* @return A configured OAuth2 service object.
111+
*/
112+
function getMeetService_() {
113+
return OAuth2.createService('Google Meet API')
114+
.setAuthorizationBaseUrl(CLIENT_SECRETS.web.auth_uri)
115+
.setTokenUrl(CLIENT_SECRETS.web.token_uri)
116+
.setClientId(CLIENT_SECRETS.web.client_id)
117+
.setClientSecret(CLIENT_SECRETS.web.client_secret)
118+
.setScope('https://www.googleapis.com/auth/meetings.space.created')
119+
.setCallbackFunction('meetAuthCallback')
120+
.setCache(CacheService.getUserCache())
121+
.setPropertyStore(PropertiesService.getUserProperties())
122+
.setParam('access_type', 'offline')
123+
.setParam('prompt', 'consent');
124+
}
125+
126+
/**
127+
* Function that handles callback requests from the OAuth2 authorization flow
128+
* for a Meet API OAuth2 service.
129+
*
130+
* @param {Object} request The request data received from the callback function.
131+
* @return {HtmlOutput} a success or denied HTML message to display to the user.
132+
*/
133+
function meetAuthCallback(request) {
134+
var meetService = getMeetService_();
135+
var authorized = meetService.handleCallback(request);
136+
if (authorized) {
137+
return HtmlService.createHtmlOutput('Success! You can close this tab.');
138+
} else {
139+
return HtmlService.createHtmlOutput('Denied. You can close this tab.');
140+
}
141+
}
142+
143+
/**
144+
* Reset user's credentials for a Meet API OAuth2 service.
145+
*/
146+
function resetMeetServiceAuth() {
147+
getMeetService_().reset();
148+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Google Chat Connectivity App
2+
3+
This sample demonstrates how to create a Google Chat app that requests
4+
authorization from the user to make API calls on their behalf. The first
5+
time the user interacts with the app, it requests offline OAuth credentials for the
6+
user and saves them to storage. If the user interacts with the app
7+
again, the saved credentials are used so the app can make API calls on behalf of the
8+
user without asking for authorization again. Once saved, the OAuth credentials could
9+
even be used without any further user interactions.
10+
11+
This app is built using Apps Script and leverages Google's OAuth2 for authorization
12+
and Apps Script's User Properties for data storage. It replies with a Chat message
13+
that contains the link to a Meet space that was created calling the Google Meet API
14+
with their consent.
15+
16+
**Key Features:**
17+
18+
* **User Authorization:** Securely requests user consent to call Meet API with
19+
their credentials.
20+
* **Meet API Integration:** Calls Meet REST API to create a new Meet space on behalf
21+
of the user.
22+
* **Google Chat Integration:** Responds to DMs and @mentions in Google Chat. If
23+
necessary, request configuration to start an OAuth authorization flow.
24+
* **Apps Script Deployment:** Provides step-by-step instructions for deploying
25+
to Apps Script.
26+
27+
## Prerequisites
28+
29+
* **Apps Script Project:** [Create](https://script.google.com/home/projects/create)
30+
* **Google Cloud Project:** [Create](https://console.cloud.google.com/projectcreate)
31+
32+
## Deployment Steps
33+
34+
1. **Enable APIs:**
35+
36+
* Enable the Meet, and Google Chat APIs using the
37+
[console](https://console.cloud.google.com/apis/enableflow?apiid=meet.googleapis.com,chat.googleapis.com).
38+
39+
1. **Deploy Apps Script Project:**
40+
41+
* Open the project from [Apps Script console](ttps://script.google.com).
42+
* In `Project Settings`, enable the option
43+
`Show "appsscript.json" manifest file in editor` and copy the `Script ID`.
44+
* In `Editor`, replace the source files `appsscript.json` and `Code.gs` with
45+
the ones found in this directory.
46+
* Click `Deploy` then `Test deployments` from the top right corner.
47+
* In the opened dialog, click `Install` and copy the `Head Deployment ID`.
48+
49+
1. **Create and Use OAuth Client ID:**
50+
51+
* In your Google Cloud project, go to
52+
[APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials).
53+
* Click `Create Credentials > OAuth client ID`.
54+
* Select `Web application` as the application type.
55+
* Add `https://script.google.com/macros/d/<Script ID from the previous step>/usercallback`
56+
to `Authorized redirect URIs`.
57+
* Download the JSON file and rename it to `client_secrets.json`.
58+
59+
1. **Configure Apps Script Project Auth:**
60+
61+
* Go to back to the project from [Apps Script console](ttps://script.google.com).
62+
* In `Editor`, open the source file `Code.gs`.
63+
* Set the varibale value `CLIENT_SECRETS` to the entire content of the file
64+
`client_secrets.json` from the previous step.
65+
* Save to automatically deploy the change.
66+
67+
## Create the Google Chat app
68+
69+
* Go to
70+
[Google Chat API](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat)
71+
and click `Configuration`.
72+
* In **App name**, enter `Connectivity App`.
73+
* In **Avatar URL**, enter `https://developers.google.com/chat/images/quickstart-app-avatar.png`.
74+
* In **Description**, enter `Connectivity app`.
75+
* Under **Functionality**, select **Join spaces and group conversations**.
76+
* Under **Connection settings**, select **Apps Script** and enter the
77+
`Head Deployment ID` (obtained in the previous deployment steps) in
78+
**Deployment ID**.
79+
* Under **Commands**, click **Add a command**, and click **Done** after setting:
80+
* **Command Id** to `1`.
81+
* **Description** and **Quick command name** to `Logout`.
82+
* Under **Visibility**, select **Make this Google Chat app available to specific
83+
people and groups in your domain** and enter your email address.
84+
* Click **Save**.
85+
86+
## Interact with the App
87+
88+
* Message the app.
89+
* Follow the authorization link to grant the app access to your account.
90+
* Once authorization is complete, the app will reply with a link to the newly
91+
created Meet space.
92+
* Message the app again, it will reply without asking for authorization.
93+
* Execute the quick command `Logout`, it will deauthorizes the app.
94+
95+
## Related Topics
96+
97+
* [Authenticate and authorize Chat apps and Google Chat API requests](https://developers.google.com/workspace/chat/authenticate-authorize)
98+
* [Build Google Chat interfaces](https://developers.google.com/workspace/add-ons/chat/build)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"timeZone": "America/New_York",
3+
"exceptionLogging": "STACKDRIVER",
4+
"runtimeVersion": "V8",
5+
"dependencies": {
6+
"libraries": [{
7+
"userSymbol": "OAuth2",
8+
"version": "43",
9+
"libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF"
10+
}]
11+
},
12+
"addOns": {
13+
"common": {
14+
"name": "Connectivity app",
15+
"logoUrl": "https://goo.gle/3SfMkjb"
16+
},
17+
"chat": {}
18+
},
19+
"oauthScopes": [
20+
"https://www.googleapis.com/auth/script.external_request"
21+
]
22+
}

java/chat/connectivity-app/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This sample demonstrates how to create a Google Chat app that requests
44
authorization from the user to make API calls on their behalf. The first
55
time the user interacts with the app, it requests offline OAuth credentials for the
6-
user and saves them to a Firestore database. If the user interacts with the app
6+
user and saves them to storage. If the user interacts with the app
77
again, the saved credentials are used so the app can make API calls on behalf of the
88
user without asking for authorization again. Once saved, the OAuth credentials could
99
even be used without any further user interactions.

node/chat/connectivity-app/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This sample demonstrates how to create a Google Chat app that requests
44
authorization from the user to make API calls on their behalf. The first
55
time the user interacts with the app, it requests offline OAuth credentials for the
6-
user and saves them to a Firestore database. If the user interacts with the app
6+
user and saves them to storage. If the user interacts with the app
77
again, the saved credentials are used so the app can make API calls on behalf of the
88
user without asking for authorization again. Once saved, the OAuth credentials could
99
even be used without any further user interactions.

node/chat/connectivity-app/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ app.post('/', async (req, res) => {
9393

9494
switch (chatEvent.appCommandPayload.appCommandMetadata.appCommandId) {
9595
case LOGOUT_COMMAND_ID:
96-
// Delete OAuth2 token from storage if any.
96+
// Delete OAuth2 credentials from storage if any.
9797
await DatabaseService.deleteUserCredentials(userName);
9898
// Reply a Chat message with confirmation
9999
return res.send({ hostAppDataAction: { chatDataAction: { createMessageAction: { message: {
@@ -115,6 +115,10 @@ app.post('/', async (req, res) => {
115115
* Returns an action response that tells Chat to request configuration for the
116116
* app. The configuration will be tied to the user who sent the event.
117117
*
118+
* @param {!string} userName The resource name of the Chat user requesting
119+
* authorization.
120+
* @param {!string} configCompleteRedirectUrl The URL to redirect to after
121+
* completing the flow.
118122
* @return {Object} An ActionResponse message request additional configuration.
119123
*/
120124
function getConfigRequestResponse(userName, configCompleteRedirectUrl) {

python/chat/connectivity-app/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This sample demonstrates how to create a Google Chat app that requests
44
authorization from the user to make API calls on their behalf. The first
55
time the user interacts with the app, it requests offline OAuth credentials for the
6-
user and saves them to a Firestore database. If the user interacts with the app
6+
user and saves them to storage. If the user interacts with the app
77
again, the saved credentials are used so the app can make API calls on behalf of the
88
user without asking for authorization again. Once saved, the OAuth credentials could
99
even be used without any further user interactions.

0 commit comments

Comments
 (0)