/*****************************************************************************\ | ( )_ _ | | _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ | | ( '_`\ ( '__)/'_` ) /'_ `\/' _ ` _ `\ /'_` )| | | |/',__)/' v `\ /'_`\ | | | (_) )| | ( (_| |( (_) || ( ) ( ) |( (_| || |_ | |\__, \| (˅) |( (_) ) | | | ,__/'(_) `\__,_)`\__ |(_) (_) (_)`\__,_)`\__)(_)(____/(_) (_)`\___/' | | | | ( )_) | | | (_) \___/' | | | | General Bots Copyright (c) Pragmatismo.io. All rights reserved. | | Licensed under the AGPL-3.0. | | | | According to our dual licensing model, this program can be used either | | under the terms of the GNU Affero General Public License, version 3, | | or under a proprietary license. | | | | The texts of the GNU Affero General Public License with an additional | | permission and of our proprietary license can be found at and | | in the LICENSE file you have received along with this program. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY, without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | "General Bots" is a registered trademark of Pragmatismo.io. | | The licensing of the program under the AGPLv3 does not imply a | | trademark license. Therefore any rights, title and interest in | | our trademarks remain entirely with us. | | | \*****************************************************************************/ import Swagger from 'swagger-client'; import { google } from 'googleapis'; import { promisify } from 'util'; import { PubSub } from '@google-cloud/pubsub'; import Fs from 'fs'; import { GBLog, GBMinInstance, GBService } from 'botlib'; import { GBServer } from '../../../src/app.js'; import { SecService } from '../../security.gbapp/services/SecService.js'; /** * Support for Google Chat. */ export class GoogleChatDirectLine extends GBService { public static conversationIds = {}; public pollInterval = 5000; public directLineClientName = 'DirectLineClient'; public directLineClient: any; public GoogleChatSubscriptionName: string; public botId: string; public min: GBMinInstance; private directLineSecret: string; pubSubClient: any; GoogleChatApiKey: any; GoogleClientEmail: any; GoogleClientPrivateKey: any; GoogleProjectId: any; constructor( min: GBMinInstance, botId, directLineSecret, GoogleChatSubscriptionName, GoogleChatApiKey, GoogleClientEmail, GoogleClientPrivateKey, GoogleProjectId ) { super(); this.min = min; this.botId = botId; this.directLineSecret = directLineSecret; this.GoogleChatSubscriptionName = GoogleChatSubscriptionName; this.GoogleChatApiKey = GoogleChatApiKey; this.GoogleClientEmail = GoogleClientEmail; this.GoogleClientPrivateKey = GoogleClientPrivateKey; this.GoogleProjectId = GoogleProjectId; this.pubSubClient = new PubSub({ projectId: this.GoogleProjectId, credentials: { client_email: GoogleClientEmail, private_key: GoogleClientPrivateKey } }); } public static async asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array); } } public async setup(setUrl) { this.directLineClient = new Swagger({ spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), usePromise: true }); const client = await this.directLineClient; client.clientAuthorizations.add( 'AuthorizationBotConnector', new Swagger.ApiKeyAuthorization('Authorization', `Bearer ${this.directLineSecret}`, 'header') ); if (setUrl) { try { const subscription = this.pubSubClient.subscription(this.GoogleChatSubscriptionName); subscription.on('message', this.receiver.bind(this)); } catch (error) { GBLog.error(`Error initializing 3rd party GoogleChat provider(1) ${error.message}`); } } } public async resetConversationId(key) { GoogleChatDirectLine.conversationIds[key] = undefined; } public async check() { GBLog.info(`GBGoogleChat: Checking server...`); } // TODO: Check service.Users.Messages.List("me"). public async receiver(message) { const event = JSON.parse(Buffer.from(message.data, 'binary').toString()); let from = ''; let fromName = ''; let text; const threadName = event.message.thread.name; if (event['type'] === 'ADDED_TO_SPACE' && event['space']['singleUserBotDm']) { } else if (event['type'] === 'MESSAGE') { text = event.message.text; fromName = event.message.sender.displayName; from = event.message.sender.email; GBLog.info(`Received message from ${from} (${fromName}): ${text}.`); } message.ack(); const sec = new SecService(); const user = await sec.ensureUser(this.min.instance.instanceId, from, from, '', 'googlechat', fromName, from); await sec.updateConversationReferenceById(user.userId, threadName); GBLog.info(`GBGoogleChat: RCV ${from}: ${text})`); const client = await this.directLineClient; const conversationId = GoogleChatDirectLine.conversationIds[from]; if (GoogleChatDirectLine.conversationIds[from] === undefined) { GBLog.info(`GBGoogleChat: Starting new conversation on Bot.`); const response = await client.Conversations.Conversations_StartConversation(); const generatedConversationId = response.obj.conversationId; GoogleChatDirectLine.conversationIds[from] = generatedConversationId; this.pollMessages(client, generatedConversationId, threadName, from, fromName); this.inputMessage(client, generatedConversationId, threadName, text, from, fromName); } else { this.inputMessage(client, conversationId, threadName, text, from, fromName); } } public inputMessage(client, conversationId, threadName, text, from, fromName) { return client.Conversations.Conversations_PostActivity({ conversationId: conversationId, activity: { textFormat: 'plain', text: text, type: 'message', mobile: from, from: { id: from, name: fromName }, replyToId: from } }); } public pollMessages(client, conversationId, threadName, from, fromName) { GBLog.info(`GBGoogleChat: Starting message polling(${from}, ${conversationId}).`); let watermark: any; const worker = async () => { try { const response = await client.Conversations.Conversations_GetActivities({ conversationId: conversationId, watermark: watermark }); watermark = response.obj.watermark; await this.printMessages(response.obj.activities, conversationId, threadName, from, fromName); } catch (err) { GBLog.error(`Error calling printMessages on GoogleChat channel ${err.data === undefined ? err : err.data}`); } }; setInterval(worker, this.pollInterval); } public async printMessages(activities, conversationId, threadName, from, fromName) { if (activities && activities.length) { // Ignore own messages. activities = activities.filter(m => m.from.id === this.botId && m.type === 'message'); if (activities.length) { // Print other messages. await GoogleChatDirectLine.asyncForEach(activities, async activity => { await this.printMessage(activity, conversationId, threadName, from, fromName); }); } } } public async printMessage(activity, conversationId, threadName, from, fromName) { let output = ''; if (activity.text) { GBLog.info(`GBGoogleChat: SND ${from}(${fromName}): ${activity.text}`); output = activity.text; } if (activity.attachments) { activity.attachments.forEach(attachment => { switch (attachment.contentType) { case 'image/png': GBLog.info(`Opening the requested image ${attachment.contentUrl}`); output += `\n${attachment.contentUrl}`; break; default: GBLog.info(`Unknown content type: ${attachment.contentType}`); } }); } await this.sendToDevice(from, conversationId, threadName, output); } public async sendToDevice(from: string, conversationId: string, threadName, msg: string) { try { let threadParts = threadName.split('/'); let spaces = threadParts[1]; let threadKey = threadParts[3]; const scopes = ['https://www.googleapis.com/auth/chat.bot']; const jwtClient = new google.auth.JWT( this.GoogleClientEmail, null, this.GoogleClientPrivateKey, scopes, null ); await jwtClient.authorize(); const chat = google.chat({version: 'v1', auth: jwtClient}); const res = await chat.spaces.messages.create({ parent: `spaces/${spaces}`, threadKey: threadKey, requestBody: { text: msg } }); GBLog.info(res); GBLog.info(`Message [${msg}] sent to ${from}: `); } catch (error) { GBLog.error(`Error sending message to GoogleChat provider ${error.message}`); } } public async sendToDeviceEx(to, conversationId, threadName, text, locale) { const minBoot = GBServer.globals.minBoot as any; text = await minBoot.conversationalService.translate( minBoot, text, locale ); await this.sendToDevice(to, conversationId, threadName, text); } }