import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { GenericModal } from "../ui/GenericModal"; import { TextField, Typography, Box, Button, IconButton, InputAdornment } from "@mui/material"; import { Visibility, VisibilityOff } from '@mui/icons-material'; import { modalStyle } from "../recorder/AddWhereCondModal"; import { useGlobalInfoStore } from '../../context/globalInfo'; import { getStoredRecording, updateRecording } from '../../api/storage'; import { WhereWhatPair } from 'maxun-core'; interface RobotMeta { name: string; id: string; createdAt: string; pairs: number; updatedAt: string; params: any[]; } interface RobotWorkflow { workflow: WhereWhatPair[]; } interface ScheduleConfig { runEvery: number; runEveryUnit: 'MINUTES' | 'HOURS' | 'DAYS' | 'WEEKS' | 'MONTHS'; startFrom: 'SUNDAY' | 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY'; atTimeStart?: string; atTimeEnd?: string; timezone: string; lastRunAt?: Date; nextRunAt?: Date; cronExpression?: string; } export interface RobotSettings { id: string; userId?: number; recording_meta: RobotMeta; 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; schedule?: ScheduleConfig | null; } interface RobotSettingsProps { isOpen: boolean; handleStart: (settings: RobotSettings) => void; handleClose: () => void; initialSettings?: RobotSettings | null; } interface CredentialInfo { value: string; type: string; } interface Credentials { [key: string]: CredentialInfo; } interface CredentialVisibility { [key: string]: boolean; } interface GroupedCredentials { passwords: string[]; emails: string[]; usernames: string[]; others: string[]; } interface ScrapeListLimit { pairIndex: number; actionIndex: number; argIndex: number; currentLimit: number; } export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => { const { t } = useTranslation(); const [credentials, setCredentials] = useState({}); const { recordingId, notify, setRerenderRobots } = useGlobalInfoStore(); const [robot, setRobot] = useState(null); const [credentialGroups, setCredentialGroups] = useState({ passwords: [], emails: [], usernames: [], others: [] }); const [showPasswords, setShowPasswords] = useState({}); const [scrapeListLimits, setScrapeListLimits] = useState([]); const isEmailPattern = (value: string): boolean => { return value.includes('@'); }; const isUsernameSelector = (selector: string): boolean => { return selector.toLowerCase().includes('username') || selector.toLowerCase().includes('user') || selector.toLowerCase().includes('email'); }; const determineCredentialType = (selector: string, info: CredentialInfo): 'password' | 'email' | 'username' | 'other' => { if (info.type === 'password' || selector.toLowerCase().includes('password')) { return 'password'; } if (isEmailPattern(info.value) || selector.toLowerCase().includes('email')) { return 'email'; } if (isUsernameSelector(selector)) { return 'username'; } return 'other'; }; useEffect(() => { if (isOpen) { getRobot(); } }, [isOpen]); useEffect(() => { if (robot?.recording?.workflow) { const extractedCredentials = extractInitialCredentials(robot.recording.workflow); setCredentials(extractedCredentials); setCredentialGroups(groupCredentialsByType(extractedCredentials)); findScrapeListLimits(robot.recording.workflow); } }, [robot]); const findScrapeListLimits = (workflow: WhereWhatPair[]) => { const limits: ScrapeListLimit[] = []; workflow.forEach((pair, pairIndex) => { if (!pair.what) return; pair.what.forEach((action, actionIndex) => { if (action.action === 'scrapeList' && action.args && action.args.length > 0) { // Check if first argument has a limit property const arg = action.args[0]; if (arg && typeof arg === 'object' && 'limit' in arg) { limits.push({ pairIndex, actionIndex, argIndex: 0, currentLimit: arg.limit }); } } }); }); setScrapeListLimits(limits); }; function extractInitialCredentials(workflow: any[]): Credentials { const credentials: Credentials = {}; const isPrintableCharacter = (char: string): boolean => { return char.length === 1 && !!char.match(/^[\x20-\x7E]$/); }; workflow.forEach(step => { if (!step.what) return; let currentSelector = ''; let currentValue = ''; let currentType = ''; let i = 0; while (i < step.what.length) { const action = step.what[i]; if (!action.action || !action.args?.[0]) { i++; continue; } const selector = action.args[0]; // Handle full word type actions first if (action.action === 'type' && action.args?.length >= 2 && typeof action.args[1] === 'string' && action.args[1].length > 1) { if (!credentials[selector]) { credentials[selector] = { value: action.args[1], type: action.args[2] || 'text' }; } i++; continue; } // Handle character-by-character sequences (both type and press) if ((action.action === 'type' || action.action === 'press') && action.args?.length >= 2 && typeof action.args[1] === 'string') { if (selector !== currentSelector) { if (currentSelector && currentValue) { credentials[currentSelector] = { value: currentValue, type: currentType || 'text' }; } currentSelector = selector; currentValue = credentials[selector]?.value || ''; currentType = action.args[2] || credentials[selector]?.type || 'text'; } const character = action.args[1]; if (isPrintableCharacter(character)) { currentValue += character; } else if (character === 'Backspace') { currentValue = currentValue.slice(0, -1); } if (!currentType && action.args[2]?.toLowerCase() === 'password') { currentType = 'password'; } let j = i + 1; while (j < step.what.length) { const nextAction = step.what[j]; if (!nextAction.action || !nextAction.args?.[0] || nextAction.args[0] !== selector || (nextAction.action !== 'type' && nextAction.action !== 'press')) { break; } if (nextAction.args[1] === 'Backspace') { currentValue = currentValue.slice(0, -1); } else if (isPrintableCharacter(nextAction.args[1])) { currentValue += nextAction.args[1]; } j++; } credentials[currentSelector] = { value: currentValue, type: currentType }; i = j; } else { i++; } } if (currentSelector && currentValue) { credentials[currentSelector] = { value: currentValue, type: currentType || 'text' }; } }); return credentials; } const groupCredentialsByType = (credentials: Credentials): GroupedCredentials => { return Object.entries(credentials).reduce((acc: GroupedCredentials, [selector, info]) => { const credentialType = determineCredentialType(selector, info); switch (credentialType) { case 'password': acc.passwords.push(selector); break; case 'email': acc.emails.push(selector); break; case 'username': acc.usernames.push(selector); break; default: acc.others.push(selector); } return acc; }, { passwords: [], emails: [], usernames: [], others: [] }); }; const getRobot = async () => { if (recordingId) { const robot = await getStoredRecording(recordingId); setRobot(robot); } else { notify('error', t('robot_edit.notifications.update_failed')); } }; const handleClickShowPassword = (selector: string) => { setShowPasswords(prev => ({ ...prev, [selector]: !prev[selector] })); }; const handleRobotNameChange = (newName: string) => { setRobot((prev) => prev ? { ...prev, recording_meta: { ...prev.recording_meta, name: newName } } : prev ); }; const handleCredentialChange = (selector: string, value: string) => { setCredentials(prev => ({ ...prev, [selector]: { ...prev[selector], value } })); }; const handleLimitChange = (pairIndex: number, actionIndex: number, argIndex: number, newLimit: number) => { setRobot((prev) => { if (!prev) return prev; const updatedWorkflow = [...prev.recording.workflow]; if ( updatedWorkflow.length > pairIndex && updatedWorkflow[pairIndex]?.what && updatedWorkflow[pairIndex].what.length > actionIndex && updatedWorkflow[pairIndex].what[actionIndex].args && updatedWorkflow[pairIndex].what[actionIndex].args.length > argIndex ) { updatedWorkflow[pairIndex].what[actionIndex].args[argIndex].limit = newLimit; setScrapeListLimits(prev => { return prev.map(item => { if (item.pairIndex === pairIndex && item.actionIndex === actionIndex && item.argIndex === argIndex) { return { ...item, currentLimit: newLimit }; } return item; }); }); } return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } }; }); }; const handleTargetUrlChange = (newUrl: string) => { setRobot((prev) => { if (!prev) return prev; const updatedWorkflow = [...prev.recording.workflow]; const lastPairIndex = updatedWorkflow.length - 1; if (lastPairIndex >= 0) { const gotoAction = updatedWorkflow[lastPairIndex]?.what?.find(action => action.action === "goto"); if (gotoAction && gotoAction.args && gotoAction.args.length > 0) { gotoAction.args[0] = newUrl; } } return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } }; }); }; const renderAllCredentialFields = () => { return ( <> {renderCredentialFields( credentialGroups.usernames, t('Username'), 'text' )} {renderCredentialFields( credentialGroups.emails, t('Email'), 'text' )} {renderCredentialFields( credentialGroups.passwords, t('Password'), 'password' )} {renderCredentialFields( credentialGroups.others, t('Other'), 'text' )} ); }; const renderCredentialFields = (selectors: string[], headerText: string, defaultType: 'text' | 'password' = 'text') => { if (selectors.length === 0) return null; return ( <> {selectors.map((selector, index) => { const isVisible = showPasswords[selector]; return ( handleCredentialChange(selector, e.target.value)} style={{ marginBottom: '20px' }} InputProps={{ endAdornment: ( handleClickShowPassword(selector)} edge="end" disabled={!credentials[selector]?.value} > {isVisible ? : } ), }} /> ); })} ); }; const renderScrapeListLimitFields = () => { if (scrapeListLimits.length === 0) return null; return ( <> {t('List Limits')} {scrapeListLimits.map((limitInfo, index) => ( { const value = parseInt(e.target.value, 10); if (value >= 1) { handleLimitChange( limitInfo.pairIndex, limitInfo.actionIndex, limitInfo.argIndex, value ); } }} inputProps={{ min: 1 }} style={{ marginBottom: '20px' }} /> ))} ); }; const handleSave = async () => { if (!robot) return; try { const credentialsForPayload = Object.entries(credentials).reduce((acc, [selector, info]) => { const enforceType = info.type === 'password' ? 'password' : 'text'; acc[selector] = { value: info.value, type: enforceType }; return acc; }, {} as Record); const lastPair = robot.recording.workflow[robot.recording.workflow.length - 1]; const targetUrl = lastPair?.what.find(action => action.action === "goto")?.args?.[0]; const payload = { name: robot.recording_meta.name, limits: scrapeListLimits.map(limit => ({ pairIndex: limit.pairIndex, actionIndex: limit.actionIndex, argIndex: limit.argIndex, limit: limit.currentLimit })), credentials: credentialsForPayload, targetUrl: targetUrl, }; const success = await updateRecording(robot.recording_meta.id, payload); if (success) { setRerenderRobots(true); notify('success', t('robot_edit.notifications.update_success')); handleStart(robot); handleClose(); } else { notify('error', t('robot_edit.notifications.update_failed')); } } catch (error) { notify('error', t('robot_edit.notifications.update_error')); console.error('Error updating robot:', error); } }; const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1]; const targetUrl = lastPair?.what.find(action => action.action === "goto")?.args?.[0]; return ( <> {t('robot_edit.title')} {robot && ( <> handleRobotNameChange(e.target.value)} style={{ marginBottom: '20px' }} /> handleTargetUrlChange(e.target.value)} style={{ marginBottom: '20px' }} /> {renderScrapeListLimitFields()} {(Object.keys(credentials).length > 0) && ( <> {t('Input Texts')} {renderAllCredentialFields()} )} )} ); };