Reputation: 30168
We've been trying to update our ChatBots to use threaded replies, but we're running into issues, specifically when responding to a brand new message in the Space. For example, if a user says:
@TheChatBot help!
We want the bot to create a threaded reply to that message. Instead, it creates a new message at the top level of the space.
Interestingly, if we mention the bot from WITHIN a thread, it works correctly.
We tried many combinations of message.thread
values (e.g. { threadKey: ...}
, {name: ...}
) and none of the combinations worked for the message outside of a thread.
Here's a snippet of the ChatBot gs code:
function onMessage(event) {
...
var message = event.message;
var threadName = message.thread ? message.thread.name : null;
...
return {
text: ...,
threadReply: true,
thread: {
name: threadName
}
};
}
Upvotes: 1
Views: 465
Reputation: 11
function sendThreadedReply(messageUrl, replyText) {
// Input validation
if (!messageUrl || !replyText) {
throw new Error('Message URL and reply text are required');
}
// Extract space name and message ID from the URL
let spaceName, messageId;
try {
// Remove any query parameters
messageUrl = messageUrl.split('?')[0];
const parts = messageUrl.split('/');
if (parts.includes('room')) {
const roomIndex = parts.indexOf('room');
spaceName = parts[roomIndex + 1];
messageId = parts[parts.length - 1];
} else if (parts.includes('spaces')) {
const spacesIndex = parts.indexOf('spaces');
spaceName = parts[spacesIndex + 1];
messageId = parts[parts.length - 1];
} else {
throw new Error('Unable to parse space name and message ID from URL');
}
} catch (error) {
Logger.log('Error parsing URL: ' + error.toString());
throw new Error('Invalid message URL format: ' + error.message);
}
// Construct the API endpoint with thread parameter
const endpoint = `https://chat.googleapis.com/v1/spaces/${spaceName}/messages?messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD`;
// Prepare the message payload with thread name
const payload = {
text: replyText,
thread: {
name: `spaces/${spaceName}/threads/${messageId}`
}
};
// Get OAuth token
let accessToken;
try {
accessToken = ScriptApp.getOAuthToken();
Logger.log('Successfully obtained OAuth token');
} catch (error) {
Logger.log('Error getting OAuth token: ' + error.toString());
throw new Error('Failed to obtain OAuth token. Make sure you have the necessary permissions.');
}
// Prepare request options
const options = {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
// Send the request
try {
const response = UrlFetchApp.fetch(endpoint, options);
const responseCode = response.getResponseCode();
const responseText = response.getContentText();
Logger.log('Response code: ' + responseCode);
Logger.log('Response body: ' + responseText);
if (responseCode === 200) {
const responseBody = JSON.parse(responseText);
return responseBody;
} else {
throw new Error(`API returned status code ${responseCode}: ${responseText}`);
}
} catch (error) {
Logger.log('Error sending request: ' + error.toString());
throw new Error('Failed to send reply: ' + error.message);
}
}
this code worked for me. You can use service account authentication or oauth2 depending on your usecase. I have used oauth2.
Threaded reply using webhooks and no oauth
function sendThreadedMessageByWebhook(webhookUrl, message, messageUrl) {
// Extract thread name from message URL
function getThreadNameFromUrl(url) {
// Remove any query parameters
url = url.split('?')[0];
const parts = url.split('/');
let spaceName, messageId;
if (parts.includes('room')) {
const roomIndex = parts.indexOf('room');
spaceName = parts[roomIndex + 1];
messageId = parts[parts.length - 1];
} else if (parts.includes('spaces')) {
const spacesIndex = parts.indexOf('spaces');
spaceName = parts[spacesIndex + 1];
messageId = parts[parts.length - 1];
} else {
throw new Error('Unable to parse space name and message ID from URL');
}
return `spaces/${spaceName}/threads/${messageId}`;
}
// Add messageReplyOption parameter to webhook URL
webhookUrl = webhookUrl + "&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD";
// Get thread name from message URL
const threadName = getThreadNameFromUrl(messageUrl);
// Prepare the message payload
const payload = {
text: message,
thread: {
name: threadName
}
};
// Prepare request options
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8'
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
// Send the request
try {
const response = UrlFetchApp.fetch(webhookUrl, options);
const responseCode = response.getResponseCode();
const responseText = response.getContentText();
Logger.log('Response code: ' + responseCode);
Logger.log('Response body: ' + responseText);
return {
responseCode: responseCode,
response: responseText
};
} catch (error) {
Logger.log('Error sending request: ' + error.toString());
throw new Error('Failed to send reply: ' + error.message);
}
}
Upvotes: 1
Reputation: 4048
My investigation revealed that Google Chat currently restricts response placement to the top-level space name, even though individual thread names (messages where the chatbot was mentioned) are readily available within the onMessage
response (e.g.,spaces/SPACE_ID/threads/THREAD_ID
).
Official documentation on Processing or Responding to Interaction Events confirms that Chat bots are currently limited to "Insert content to space" (see image below) for their replies, suggesting this behavior is intentional:
I attempted to formulate a workaround following the guide spaces.message.create
. In this process, I intended for the chatbot to create a message on the thread where it has been mentioned, with use of the thread name. However, it appears there is a limitation in the Chat API. Specifically, one of the required parameters is a top-level Space name instead of a thread name.
Having said that, I would recommend that you consider submitting a feature idea on Google Issue Tracker. I believe this would be an ideal request for the chatbot, given its use cases such as the primary goal mentioned in your post.
Upvotes: 1