@@ -19,6 +19,11 @@ interface RobotAttributes {
|
|||||||
id: string;
|
id: string;
|
||||||
recording_meta: RobotMeta;
|
recording_meta: RobotMeta;
|
||||||
recording: RobotWorkflow;
|
recording: RobotWorkflow;
|
||||||
|
google_sheet_email?: string | null;
|
||||||
|
google_sheet_name?: string | null;
|
||||||
|
google_sheet_id?: string | null;
|
||||||
|
google_access_token?: string | null;
|
||||||
|
google_refresh_token?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RobotCreationAttributes extends Optional<RobotAttributes, 'id'> { }
|
interface RobotCreationAttributes extends Optional<RobotAttributes, 'id'> { }
|
||||||
@@ -27,6 +32,11 @@ class Robot extends Model<RobotAttributes, RobotCreationAttributes> implements R
|
|||||||
public id!: string;
|
public id!: string;
|
||||||
public recording_meta!: RobotMeta;
|
public recording_meta!: RobotMeta;
|
||||||
public recording!: RobotWorkflow;
|
public recording!: RobotWorkflow;
|
||||||
|
public google_sheet_email!: string | null;
|
||||||
|
public google_sheet_name?: string | null;
|
||||||
|
public google_sheet_id?: string | null;
|
||||||
|
public google_access_token!: string | null;
|
||||||
|
public google_refresh_token!: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Robot.init(
|
Robot.init(
|
||||||
@@ -44,6 +54,26 @@ Robot.init(
|
|||||||
type: DataTypes.JSONB,
|
type: DataTypes.JSONB,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
google_sheet_email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
google_sheet_name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
google_sheet_id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
google_access_token: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
google_refresh_token: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ interface UserAttributes {
|
|||||||
proxy_password?: string | null;
|
proxy_password?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional fields for creating a new user
|
|
||||||
interface UserCreationAttributes extends Optional<UserAttributes, 'id'> { }
|
interface UserCreationAttributes extends Optional<UserAttributes, 'id'> { }
|
||||||
|
|
||||||
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import User from '../models/User';
|
import User from '../models/User';
|
||||||
|
import Robot from '../models/Robot';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { hashPassword, comparePassword } from '../utils/auth';
|
import { hashPassword, comparePassword } from '../utils/auth';
|
||||||
import { requireSignIn } from '../middlewares/auth';
|
import { requireSignIn } from '../middlewares/auth';
|
||||||
import { genAPIKey } from '../utils/api';
|
import { genAPIKey } from '../utils/api';
|
||||||
|
import { google } from 'googleapis';
|
||||||
export const router = Router();
|
export const router = Router();
|
||||||
|
|
||||||
interface AuthenticatedRequest extends Request {
|
interface AuthenticatedRequest extends Request {
|
||||||
@@ -163,3 +165,192 @@ router.delete('/delete-api-key', requireSignIn, async (req, res) => {
|
|||||||
return res.status(500).json({ message: 'Error deleting API key', error: error.message });
|
return res.status(500).json({ message: 'Error deleting API key', error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const oauth2Client = new google.auth.OAuth2(
|
||||||
|
process.env.GOOGLE_CLIENT_ID,
|
||||||
|
process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
process.env.GOOGLE_REDIRECT_URI
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 1: Redirect to Google for authentication
|
||||||
|
router.get('/google', (req, res) => {
|
||||||
|
const { robotId } = req.query;
|
||||||
|
if (!robotId) {
|
||||||
|
return res.status(400).json({ message: 'Robot ID is required' });
|
||||||
|
}
|
||||||
|
const scopes = [
|
||||||
|
'https://www.googleapis.com/auth/spreadsheets',
|
||||||
|
'https://www.googleapis.com/auth/userinfo.email',
|
||||||
|
'https://www.googleapis.com/auth/drive.readonly',
|
||||||
|
];
|
||||||
|
const url = oauth2Client.generateAuthUrl({
|
||||||
|
access_type: 'offline',
|
||||||
|
prompt: 'consent', // Ensures you get a refresh token on first login
|
||||||
|
scope: scopes,
|
||||||
|
state: robotId.toString(),
|
||||||
|
});
|
||||||
|
res.redirect(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Handle Google OAuth callback
|
||||||
|
router.get('/google/callback', requireSignIn, async (req, res) => {
|
||||||
|
const { code, state } = req.query;
|
||||||
|
try {
|
||||||
|
if (!state) {
|
||||||
|
return res.status(400).json({ message: 'Robot ID is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const robotId = state
|
||||||
|
|
||||||
|
// Get access and refresh tokens
|
||||||
|
if (typeof code !== 'string') {
|
||||||
|
return res.status(400).json({ message: 'Invalid code' });
|
||||||
|
}
|
||||||
|
const { tokens } = await oauth2Client.getToken(code);
|
||||||
|
oauth2Client.setCredentials(tokens);
|
||||||
|
|
||||||
|
// Get user profile from Google
|
||||||
|
const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client });
|
||||||
|
const { data: { email } } = await oauth2.userinfo.get();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({ message: 'Email not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the currently authenticated user (from `requireSignIn`)
|
||||||
|
let user = await User.findOne({ where: { id: req.user.id } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(400).json({ message: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
|
||||||
|
|
||||||
|
if (!robot) {
|
||||||
|
return res.status(400).json({ message: 'Robot not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
robot = await robot.update({
|
||||||
|
google_sheet_email: email,
|
||||||
|
google_access_token: tokens.access_token,
|
||||||
|
google_refresh_token: tokens.refresh_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
// List user's Google Sheets from their Google Drive
|
||||||
|
const drive = google.drive({ version: 'v3', auth: oauth2Client });
|
||||||
|
const response = await drive.files.list({
|
||||||
|
q: "mimeType='application/vnd.google-apps.spreadsheet'", // List only Google Sheets files
|
||||||
|
fields: 'files(id, name)', // Retrieve the ID and name of each file
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = response.data.files || [];
|
||||||
|
if (files.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'No spreadsheets found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token for session
|
||||||
|
const jwtToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as string, { expiresIn: '12h' });
|
||||||
|
res.cookie('token', jwtToken, { httpOnly: true });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Google authentication successful',
|
||||||
|
google_sheet_email: robot.google_sheet_email,
|
||||||
|
jwtToken,
|
||||||
|
files
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ message: `Google OAuth error: ${error.message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: Get data from Google Sheets
|
||||||
|
router.post('/gsheets/data', requireSignIn, async (req, res) => {
|
||||||
|
const { spreadsheetId, robotId } = req.body;
|
||||||
|
const user = await User.findByPk(req.user.id, { raw: true });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(400).json({ message: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId }, raw: true });
|
||||||
|
|
||||||
|
if (!robot) {
|
||||||
|
return res.status(400).json({ message: 'Robot not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Google OAuth credentials
|
||||||
|
oauth2Client.setCredentials({
|
||||||
|
access_token: robot.google_access_token,
|
||||||
|
refresh_token: robot.google_refresh_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheets = google.sheets({ version: 'v4', auth: oauth2Client });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch data from the spreadsheet (you can let the user choose a specific range too)
|
||||||
|
const sheetData = await sheets.spreadsheets.values.get({
|
||||||
|
spreadsheetId,
|
||||||
|
range: 'Sheet1!A1:D5', // Default range, could be dynamic based on user input
|
||||||
|
});
|
||||||
|
res.json(sheetData.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ message: `Error accessing Google Sheets: ${error.message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 4: Get user's Google Sheets files (new route)
|
||||||
|
router.get('/gsheets/files', requireSignIn, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const robotId = req.query.robotId;
|
||||||
|
const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId }, raw:true });
|
||||||
|
|
||||||
|
if (!robot) {
|
||||||
|
return res.status(400).json({ message: 'Robot not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2Client.setCredentials({
|
||||||
|
access_token: robot.google_access_token,
|
||||||
|
refresh_token: robot.google_refresh_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
// List user's Google Sheets files from their Google Drive
|
||||||
|
const drive = google.drive({ version: 'v3', auth: oauth2Client });
|
||||||
|
const response = await drive.files.list({
|
||||||
|
q: "mimeType='application/vnd.google-apps.spreadsheet'",
|
||||||
|
fields: 'files(id, name)',
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = response.data.files || [];
|
||||||
|
if (files.length === 0) {
|
||||||
|
return res.status(404).json({ message: 'No spreadsheets found.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(files);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('Error fetching Google Sheets files:', error);
|
||||||
|
res.status(500).json({ message: `Error retrieving Google Sheets files: ${error.message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 5: Update robot's google_sheet_id when a Google Sheet is selected
|
||||||
|
router.post('/gsheets/update', requireSignIn, async (req, res) => {
|
||||||
|
const { spreadsheetId, spreadsheetName, robotId } = req.body;
|
||||||
|
|
||||||
|
if (!spreadsheetId || !robotId) {
|
||||||
|
return res.status(400).json({ message: 'Spreadsheet ID and Robot ID are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
|
||||||
|
|
||||||
|
if (!robot) {
|
||||||
|
return res.status(404).json({ message: 'Robot not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await robot.update({ google_sheet_id: spreadsheetId, google_sheet_name: spreadsheetName });
|
||||||
|
|
||||||
|
res.json({ message: 'Robot updated with selected Google Sheet ID' });
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ message: `Error updating robot: ${error.message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { requireSignIn } from '../middlewares/auth';
|
|||||||
import Robot from '../models/Robot';
|
import Robot from '../models/Robot';
|
||||||
import Run from '../models/Run';
|
import Run from '../models/Run';
|
||||||
import { BinaryOutputService } from '../storage/mino';
|
import { BinaryOutputService } from '../storage/mino';
|
||||||
import { workflowQueue } from '../worker';
|
// import { workflowQueue } from '../worker';
|
||||||
|
|
||||||
export const router = Router();
|
export const router = Router();
|
||||||
|
|
||||||
@@ -37,6 +37,23 @@ router.get('/recordings', requireSignIn, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET endpoint for getting a recording.
|
||||||
|
*/
|
||||||
|
router.get('/recordings/:id', requireSignIn, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await Robot.findOne({
|
||||||
|
where: { 'recording_meta.id': req.params.id },
|
||||||
|
raw: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return res.send(data);
|
||||||
|
} catch (e) {
|
||||||
|
logger.log('info', 'Error while reading recordings');
|
||||||
|
return res.send(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE endpoint for deleting a recording from the storage.
|
* DELETE endpoint for deleting a recording from the storage.
|
||||||
*/
|
*/
|
||||||
@@ -202,13 +219,17 @@ router.post('/runs/run/:id', requireSignIn, async (req, res) => {
|
|||||||
serializableOutput: interpretationInfo.serializableOutput,
|
serializableOutput: interpretationInfo.serializableOutput,
|
||||||
binaryOutput: uploadedBinaryOutput,
|
binaryOutput: uploadedBinaryOutput,
|
||||||
});
|
});
|
||||||
googleSheetUpdateTasks[req.params.id] = {
|
try {
|
||||||
name: plainRun.name,
|
googleSheetUpdateTasks[plainRun.runId] = {
|
||||||
runId: plainRun.runId,
|
robotId: plainRun.robotMetaId,
|
||||||
status: 'pending',
|
runId: plainRun.runId,
|
||||||
retries: 5,
|
status: 'pending',
|
||||||
};
|
retries: 5,
|
||||||
processGoogleSheetUpdates();
|
};
|
||||||
|
processGoogleSheetUpdates();
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.log('error', `Failed to update Google Sheet for run: ${plainRun.runId}: ${err.message}`);
|
||||||
|
}
|
||||||
return res.send(true);
|
return res.send(true);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Could not destroy browser');
|
throw new Error('Could not destroy browser');
|
||||||
@@ -282,16 +303,16 @@ router.put('/schedule/:id/', requireSignIn, async (req, res) => {
|
|||||||
const runId = uuid();
|
const runId = uuid();
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
await workflowQueue.add(
|
// await workflowQueue.add(
|
||||||
'run workflow',
|
// 'run workflow',
|
||||||
{ id, runId, userId },
|
// { id, runId, userId },
|
||||||
{
|
// {
|
||||||
repeat: {
|
// repeat: {
|
||||||
pattern: cronExpression,
|
// pattern: cronExpression,
|
||||||
tz: timezone
|
// tz: timezone
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: 'success',
|
message: 'success',
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { google } from "googleapis";
|
import { google } from "googleapis";
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import logger from "../../logger";
|
import logger from "../../logger";
|
||||||
import { readFile } from "../storage";
|
import Run from "../../models/Run";
|
||||||
|
import Robot from "../../models/Robot";
|
||||||
|
|
||||||
interface GoogleSheetUpdateTask {
|
interface GoogleSheetUpdateTask {
|
||||||
name: string;
|
robotId: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
status: 'pending' | 'completed' | 'failed';
|
status: 'pending' | 'completed' | 'failed';
|
||||||
retries: number;
|
retries: number;
|
||||||
@@ -14,80 +14,101 @@ const MAX_RETRIES = 5;
|
|||||||
|
|
||||||
export let googleSheetUpdateTasks: { [runId: string]: GoogleSheetUpdateTask } = {};
|
export let googleSheetUpdateTasks: { [runId: string]: GoogleSheetUpdateTask } = {};
|
||||||
|
|
||||||
|
export async function updateGoogleSheet(robotId: string, runId: string) {
|
||||||
// *** Temporary Path to the JSON file that will store the integration details ***
|
|
||||||
const getIntegrationsFilePath = (fileName: string) => path.join(__dirname, `integrations-${fileName}.json`);
|
|
||||||
|
|
||||||
export function loadIntegrations(fileName: string) {
|
|
||||||
const filePath = getIntegrationsFilePath(fileName);
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
const data = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
return JSON.parse(data);
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveIntegrations(fileName: string, integrations: any) {
|
|
||||||
const filePath = getIntegrationsFilePath(fileName);
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(integrations, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateGoogleSheet(fileName: string, runId: string) {
|
|
||||||
try {
|
try {
|
||||||
const run = await readFile(`./../storage/runs/${fileName}_${runId}.json`);
|
const run = await Run.findOne({ where: { runId } });
|
||||||
const parsedRun = JSON.parse(run);
|
|
||||||
|
|
||||||
if (parsedRun.status === 'success' && parsedRun.serializableOutput) {
|
if (!run) {
|
||||||
const data = parsedRun.serializableOutput['item-0'] as { [key: string]: any }[];
|
throw new Error(`Run not found for runId: ${runId}`);
|
||||||
const integrationConfig = await loadIntegrations(fileName);
|
}
|
||||||
|
|
||||||
if (integrationConfig) {
|
const plainRun = run.toJSON();
|
||||||
const { fileName, spreadsheetId, range, credentials } = integrationConfig;
|
|
||||||
|
if (plainRun.status === 'success' && plainRun.serializableOutput) {
|
||||||
if (fileName && spreadsheetId && range && credentials) {
|
const data = plainRun.serializableOutput['item-0'] as { [key: string]: any }[];
|
||||||
// Convert data to Google Sheets format (headers and rows)
|
|
||||||
const headers = Object.keys(data[0]);
|
const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
|
||||||
const rows = data.map((row: { [key: string]: any }) => Object.values(row));
|
|
||||||
const outputData = [headers, ...rows];
|
if (!robot) {
|
||||||
|
throw new Error(`Robot not found for robotId: ${robotId}`);
|
||||||
await writeDataToSheet(fileName, spreadsheetId, range, outputData);
|
}
|
||||||
logger.log('info', `Data written to Google Sheet successfully for ${fileName}_${runId}`);
|
|
||||||
}
|
const plainRobot = robot.toJSON();
|
||||||
}
|
|
||||||
logger.log('error', `Google Sheet integration not configured for ${fileName}_${runId}`);
|
const spreadsheetId = plainRobot.google_sheet_id;
|
||||||
|
if (plainRobot.google_sheet_email && spreadsheetId) {
|
||||||
|
console.log(`Preparing to write data to Google Sheet for robot: ${robotId}, spreadsheetId: ${spreadsheetId}`);
|
||||||
|
|
||||||
|
const headers = Object.keys(data[0]);
|
||||||
|
const rows = data.map((row: { [key: string]: any }) => Object.values(row));
|
||||||
|
const outputData = [headers, ...rows];
|
||||||
|
|
||||||
|
await writeDataToSheet(robotId, spreadsheetId, outputData);
|
||||||
|
console.log(`Data written to Google Sheet successfully for Robot: ${robotId} and Run: ${runId}`);
|
||||||
|
} else {
|
||||||
|
console.log('Google Sheets integration not configured.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Run status is not success or serializableOutput is missing.');
|
||||||
}
|
}
|
||||||
logger.log('error', `Run not successful or no data to update for ${fileName}_${runId}`);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.log('error', `Failed to write data to Google Sheet for ${fileName}_${runId}: ${error.message}`);
|
console.error(`Failed to write data to Google Sheet for Robot: ${robotId} and Run: ${runId}: ${error.message}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function writeDataToSheet(fileName: string, spreadsheetId: string, range: string, data: any[]) {
|
export async function writeDataToSheet(robotId: string, spreadsheetId: string, data: any[]) {
|
||||||
try {
|
try {
|
||||||
const integrationCredentialsPath = getIntegrationsFilePath(fileName);
|
const robot = await Robot.findOne({ where: { 'recording_meta.id': robotId } });
|
||||||
const integrationCredentials = JSON.parse(fs.readFileSync(integrationCredentialsPath, 'utf-8'));;
|
|
||||||
|
|
||||||
const auth = new google.auth.GoogleAuth({
|
if (!robot) {
|
||||||
credentials: {
|
throw new Error(`Robot not found for robotId: ${robotId}`);
|
||||||
client_email: integrationCredentials.credentials.client_email,
|
}
|
||||||
private_key: integrationCredentials.credentials.private_key,
|
|
||||||
},
|
const plainRobot = robot.toJSON();
|
||||||
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
||||||
|
if (!plainRobot.google_access_token || !plainRobot.google_refresh_token) {
|
||||||
|
throw new Error('Google Sheets access not configured for user');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauth2Client = new google.auth.OAuth2(
|
||||||
|
process.env.GOOGLE_CLIENT_ID,
|
||||||
|
process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
process.env.GOOGLE_REDIRECT_URI
|
||||||
|
);
|
||||||
|
|
||||||
|
oauth2Client.setCredentials({
|
||||||
|
access_token: plainRobot.google_access_token,
|
||||||
|
refresh_token: plainRobot.google_refresh_token,
|
||||||
});
|
});
|
||||||
|
|
||||||
const authToken = await auth.getClient();
|
oauth2Client.on('tokens', async (tokens) => {
|
||||||
const sheets = google.sheets({ version: 'v4', auth: authToken as any });
|
if (tokens.refresh_token) {
|
||||||
|
await robot.update({ google_refresh_token: tokens.refresh_token });
|
||||||
|
}
|
||||||
|
if (tokens.access_token) {
|
||||||
|
await robot.update({ google_access_token: tokens.access_token });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheets = google.sheets({ version: 'v4', auth: oauth2Client });
|
||||||
|
|
||||||
const resource = { values: data };
|
const resource = { values: data };
|
||||||
|
console.log('Attempting to write to spreadsheet:', spreadsheetId);
|
||||||
|
|
||||||
await sheets.spreadsheets.values.append({
|
const response = await sheets.spreadsheets.values.append({
|
||||||
spreadsheetId,
|
spreadsheetId,
|
||||||
range,
|
range: 'Sheet1!A1',
|
||||||
valueInputOption: 'USER_ENTERED',
|
valueInputOption: 'USER_ENTERED',
|
||||||
requestBody: resource,
|
requestBody: resource,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log(`info`, `Data written to Google Sheet: ${spreadsheetId}, Range: ${range}`);
|
if (response.status === 200) {
|
||||||
|
console.log('Data successfully appended to Google Sheet.');
|
||||||
|
} else {
|
||||||
|
console.error('Google Sheets append failed:', response);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(`info`, `Data written to Google Sheet: ${spreadsheetId}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.log(`error`, `Error writing data to Google Sheet: ${error.message}`);
|
logger.log(`error`, `Error writing data to Google Sheet: ${error.message}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -99,25 +120,33 @@ export const processGoogleSheetUpdates = async () => {
|
|||||||
let hasPendingTasks = false;
|
let hasPendingTasks = false;
|
||||||
for (const runId in googleSheetUpdateTasks) {
|
for (const runId in googleSheetUpdateTasks) {
|
||||||
const task = googleSheetUpdateTasks[runId];
|
const task = googleSheetUpdateTasks[runId];
|
||||||
|
console.log(`Processing task for runId: ${runId}, status: ${task.status}`);
|
||||||
|
|
||||||
if (task.status === 'pending') {
|
if (task.status === 'pending') {
|
||||||
hasPendingTasks = true;
|
hasPendingTasks = true;
|
||||||
try {
|
try {
|
||||||
await updateGoogleSheet(task.name, task.runId);
|
await updateGoogleSheet(task.robotId, task.runId);
|
||||||
|
console.log(`Successfully updated Google Sheet for runId: ${runId}`);
|
||||||
delete googleSheetUpdateTasks[runId];
|
delete googleSheetUpdateTasks[runId];
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error(`Failed to update Google Sheets for run ${task.runId}:`, error);
|
||||||
if (task.retries < MAX_RETRIES) {
|
if (task.retries < MAX_RETRIES) {
|
||||||
googleSheetUpdateTasks[runId].retries += 1;
|
googleSheetUpdateTasks[runId].retries += 1;
|
||||||
|
console.log(`Retrying task for runId: ${runId}, attempt: ${task.retries}`);
|
||||||
} else {
|
} else {
|
||||||
// Mark as failed after maximum retries
|
|
||||||
googleSheetUpdateTasks[runId].status = 'failed';
|
googleSheetUpdateTasks[runId].status = 'failed';
|
||||||
|
console.log(`Max retries reached for runId: ${runId}. Marking task as failed.`);
|
||||||
}
|
}
|
||||||
console.error(`Failed to update Google Sheets for run ${task.runId}:`, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasPendingTasks) {
|
if (!hasPendingTasks) {
|
||||||
|
console.log('No pending tasks. Exiting loop.');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Waiting for 5 seconds before checking again...');
|
||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -132,7 +132,7 @@ async function executeRun(id: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
googleSheetUpdateTasks[id] = {
|
googleSheetUpdateTasks[id] = {
|
||||||
name: plainRun.name,
|
robotId: plainRun.robotMetaId,
|
||||||
runId: id,
|
runId: id,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
retries: 5,
|
retries: 5,
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ export const getStoredRuns = async (): Promise<string[] | null> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getStoredRecording = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8080/storage/recordings/${id}`);
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Couldn't retrieve stored recording ${id}`);
|
||||||
|
}
|
||||||
|
} catch(error: any) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteRecordingFromStorage = async (id: string): Promise<boolean> => {
|
export const deleteRecordingFromStorage = async (id: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.delete(`http://localhost:8080/storage/recordings/${id}`);
|
const response = await axios.delete(`http://localhost:8080/storage/recordings/${id}`);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { GenericModal } from "../atoms/GenericModal";
|
import { GenericModal } from "../atoms/GenericModal";
|
||||||
import { MenuItem, TextField, Typography } from "@mui/material";
|
import { MenuItem, Typography, CircularProgress } from "@mui/material";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import { modalStyle } from "./AddWhereCondModal";
|
import TextField from "@mui/material/TextField";
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||||
|
import { getStoredRecording } from '../../api/storage';
|
||||||
|
|
||||||
interface IntegrationProps {
|
interface IntegrationProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -11,71 +14,175 @@ interface IntegrationProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IntegrationSettings {
|
export interface IntegrationSettings {
|
||||||
credentials: string;
|
|
||||||
spreadsheetId: string;
|
spreadsheetId: string;
|
||||||
range: string;
|
spreadsheetName: string;
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationSettingsModal = ({ isOpen, handleStart, handleClose }: IntegrationProps) => {
|
export const IntegrationSettingsModal = ({ isOpen, handleStart, handleClose }: IntegrationProps) => {
|
||||||
|
|
||||||
const [settings, setSettings] = useState<IntegrationSettings>({
|
const [settings, setSettings] = useState<IntegrationSettings>({
|
||||||
credentials: '',
|
|
||||||
spreadsheetId: '',
|
spreadsheetId: '',
|
||||||
range: '',
|
spreadsheetName: '',
|
||||||
data: '',
|
data: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (field: keyof IntegrationSettings) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
const [spreadsheets, setSpreadsheets] = useState<{ id: string, name: string }[]>([]);
|
||||||
setSettings({ ...settings, [field]: e.target.value });
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { recordingId } = useGlobalInfoStore();
|
||||||
|
const [recording, setRecording] = useState<any>(null);
|
||||||
|
|
||||||
|
const authenticateWithGoogle = () => {
|
||||||
|
window.location.href = `http://localhost:8080/auth/google?robotId=${recordingId}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOAuthCallback = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8080/auth/google/callback`);
|
||||||
|
const { google_sheet_email, files } = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
setError('Error authenticating with Google');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSpreadsheetFiles = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`http://localhost:8080/auth/gsheets/files?robotId=${recordingId}`, {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
setSpreadsheets(response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching spreadsheet files:', error.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSpreadsheetSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selectedSheet = spreadsheets.find(sheet => sheet.id === e.target.value);
|
||||||
|
if (selectedSheet) {
|
||||||
|
setSettings({ ...settings, spreadsheetId: selectedSheet.id, spreadsheetName: selectedSheet.name });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGoogleSheetId = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`http://localhost:8080/auth/gsheets/update`,
|
||||||
|
{ spreadsheetId: settings.spreadsheetId, spreadsheetName: settings.spreadsheetName, robotId: recordingId },
|
||||||
|
{ withCredentials: true }
|
||||||
|
);
|
||||||
|
console.log('Google Sheet ID updated:', response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error updating Google Sheet ID:', error.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if we're on the callback URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const code = urlParams.get('code');
|
||||||
|
if (code) {
|
||||||
|
handleOAuthCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRecordingInfo = async () => {
|
||||||
|
if (!recordingId) return;
|
||||||
|
const recording = await getStoredRecording(recordingId);
|
||||||
|
if (recording) {
|
||||||
|
setRecording(recording);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchRecordingInfo();
|
||||||
|
}, [recordingId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericModal
|
<GenericModal isOpen={isOpen} onClose={handleClose}>
|
||||||
isOpen={isOpen}
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', marginLeft: '65px' }}>
|
||||||
onClose={handleClose}
|
|
||||||
modalStyle={modalStyle}
|
|
||||||
>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginLeft: '65px',
|
|
||||||
}}>
|
|
||||||
<Typography sx={{ margin: '20px 0px' }}>Google Sheets Integration</Typography>
|
<Typography sx={{ margin: '20px 0px' }}>Google Sheets Integration</Typography>
|
||||||
|
|
||||||
<TextField
|
{recording && recording.google_sheet_id ? (
|
||||||
sx={{ marginBottom: '15px' }}
|
<Typography sx={{ marginBottom: '10px' }}>
|
||||||
label="Service Account JSON"
|
Google Sheet Integrated Successfully!
|
||||||
multiline
|
<br />
|
||||||
rows={10}
|
Sheet Name: {recording.google_sheet_name}
|
||||||
required
|
<br />
|
||||||
value={settings.credentials}
|
Sheet ID: {recording.google_sheet_id}
|
||||||
onChange={handleChange('credentials')}
|
</Typography>
|
||||||
fullWidth
|
) : (
|
||||||
/>
|
<>
|
||||||
|
{!recording?.google_sheet_email ? (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={authenticateWithGoogle}
|
||||||
|
style={{ marginBottom: '15px' }}
|
||||||
|
>
|
||||||
|
Authenticate with Google
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{recording.google_sheet_email && (
|
||||||
|
<Typography sx={{ marginBottom: '10px' }}>
|
||||||
|
Logged in as: {recording.google_sheet_email}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
<TextField
|
{loading ? (
|
||||||
sx={{ marginBottom: '15px' }}
|
<CircularProgress sx={{ marginBottom: '15px' }} />
|
||||||
label="Google Spreadsheet ID"
|
) : error ? (
|
||||||
required
|
<Typography color="error">{error}</Typography>
|
||||||
value={settings.spreadsheetId}
|
) : spreadsheets.length === 0 ? (
|
||||||
onChange={handleChange('spreadsheetId')}
|
<Button
|
||||||
fullWidth
|
variant="contained"
|
||||||
/>
|
color="primary"
|
||||||
|
onClick={fetchSpreadsheetFiles}
|
||||||
|
style={{ marginBottom: '15px' }}
|
||||||
|
>
|
||||||
|
Fetch Google Spreadsheets
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TextField
|
||||||
|
sx={{ marginBottom: '15px' }}
|
||||||
|
select
|
||||||
|
label="Select Google Spreadsheet"
|
||||||
|
required
|
||||||
|
value={settings.spreadsheetId}
|
||||||
|
onChange={handleSpreadsheetSelect}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{spreadsheets.map(sheet => (
|
||||||
|
<MenuItem key={sheet.id} value={sheet.id}>
|
||||||
|
{sheet.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
<TextField
|
{settings.spreadsheetId && (
|
||||||
sx={{ marginBottom: '15px' }}
|
<Typography sx={{ marginBottom: '10px' }}>
|
||||||
label="Range (e.g., Sheet1!A1:B2)"
|
Selected Sheet: {spreadsheets.find(s => s.id === settings.spreadsheetId)?.name} (ID: {settings.spreadsheetId})
|
||||||
required
|
</Typography>
|
||||||
value={settings.range}
|
)}
|
||||||
onChange={handleChange('range')}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button variant="contained" color="primary" onClick={() => handleStart(settings)} style={{ marginTop: '10px' }}>
|
<Button
|
||||||
Submit
|
variant="contained"
|
||||||
</Button>
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
updateGoogleSheetId();
|
||||||
|
handleStart(settings);
|
||||||
|
}}
|
||||||
|
style={{ marginTop: '10px' }}
|
||||||
|
disabled={!settings.spreadsheetId || loading}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GenericModal>
|
</GenericModal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface RecordingsProps {
|
|||||||
handleEditRecording: (id: string, fileName: string) => void;
|
handleEditRecording: (id: string, fileName: string) => void;
|
||||||
handleRunRecording: (settings: RunSettings) => void;
|
handleRunRecording: (settings: RunSettings) => void;
|
||||||
handleScheduleRecording: (settings: ScheduleSettings) => void;
|
handleScheduleRecording: (settings: ScheduleSettings) => void;
|
||||||
handleIntegrateRecording: (settings: IntegrationSettings) => void;
|
handleIntegrateRecording: (id: string, settings: IntegrationSettings) => void;
|
||||||
setRecordingInfo: (id: string, name: string) => void;
|
setRecordingInfo: (id: string, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
|||||||
const [params, setParams] = useState<string[]>([]);
|
const [params, setParams] = useState<string[]>([]);
|
||||||
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
|
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
|
||||||
|
|
||||||
|
console.log(`Selected reocrding id: ${selectedRecordingId}`);
|
||||||
|
|
||||||
const handleSettingsAndIntegrate = (id: string, name: string, params: string[]) => {
|
const handleSettingsAndIntegrate = (id: string, name: string, params: string[]) => {
|
||||||
if (params.length === 0) {
|
if (params.length === 0) {
|
||||||
setIntegrateSettingsAreOpen(true);
|
setIntegrateSettingsAreOpen(true);
|
||||||
@@ -94,7 +96,7 @@ export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordi
|
|||||||
/>
|
/>
|
||||||
<IntegrationSettingsModal isOpen={integrateSettingsAreOpen}
|
<IntegrationSettingsModal isOpen={integrateSettingsAreOpen}
|
||||||
handleClose={handleIntegrateClose}
|
handleClose={handleIntegrateClose}
|
||||||
handleStart={(settings) => handleIntegrateRecording(settings)}
|
handleStart={(settings) => handleIntegrateRecording(selectedRecordingId, settings)}
|
||||||
/>
|
/>
|
||||||
<Grid container direction="column" sx={{ padding: '30px' }}>
|
<Grid container direction="column" sx={{ padding: '30px' }}>
|
||||||
<Grid item xs>
|
<Grid item xs>
|
||||||
|
|||||||
@@ -346,6 +346,10 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
resetListState();
|
resetListState();
|
||||||
|
setShowPaginationOptions(false);
|
||||||
|
setShowLimitOptions(false);
|
||||||
|
setCaptureStage('initial');
|
||||||
|
setConfirmedListTextFields({});
|
||||||
notify('error', 'Capture List Discarded');
|
notify('error', 'Capture List Discarded');
|
||||||
}, [browserSteps, stopGetList, deleteBrowserStep, resetListState]);
|
}, [browserSteps, stopGetList, deleteBrowserStep, resetListState]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user