diff --git a/backend/bot/social_media_bot/account_manager.php b/backend/bot/social_media_bot/account_manager.php new file mode 100644 index 00000000..610a8b30 --- /dev/null +++ b/backend/bot/social_media_bot/account_manager.php @@ -0,0 +1,67 @@ +con = Database::get('main'); + } + + /** + * Get an available account for a specific platform that hasn't been used too recently. + * + * @param string $platform 'facebook' or 'instagram' + * @param int $cooldownMinutes Minimum minutes since last active + * @return array|null Account details or null if none available + */ + public function getAvailableAccount($platform, $cooldownMinutes = 30) { + // Select an active account that was last active more than X minutes ago, + // or has never been active (last_active is NULL). + // Order by last_active ascending to rotate through them (least recently used first). + + $stmt = $this->con->prepare(" + SELECT id, username, total_posts, total_comments + FROM social_accounts + WHERE platform = ? + AND status = 'active' + AND (last_active IS NULL OR last_active <= DATE_SUB(NOW(), INTERVAL ? MINUTE)) + ORDER BY last_active ASC, id ASC + LIMIT 1 + "); + + $stmt->execute([$platform, $cooldownMinutes]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + /** + * Update the last active timestamp for an account + */ + public function markAccountActive($accountId) { + $stmt = $this->con->prepare("UPDATE social_accounts SET last_active = NOW() WHERE id = ?"); + $stmt->execute([$accountId]); + } + + /** + * Increment comment count + */ + public function incrementCommentCount($accountId) { + $stmt = $this->con->prepare("UPDATE social_accounts SET total_comments = total_comments + 1, last_active = NOW() WHERE id = ?"); + $stmt->execute([$accountId]); + } + + /** + * Mark an account as restricted or banned + */ + public function markAccountStatus($accountId, $status) { + if (!in_array($status, ['active', 'restricted', 'banned'])) return false; + + $stmt = $this->con->prepare("UPDATE social_accounts SET status = ? WHERE id = ?"); + return $stmt->execute([$status, $accountId]); + } +} diff --git a/backend/bot/social_media_bot/gemini_comment_generator.php b/backend/bot/social_media_bot/gemini_comment_generator.php new file mode 100644 index 00000000..90438f36 --- /dev/null +++ b/backend/bot/social_media_bot/gemini_comment_generator.php @@ -0,0 +1,81 @@ + [ + [ + "parts" => [ + ["text" => $systemInstruction . "\n\n" . $prompt] + ] + ] + ], + "generationConfig" => [ + "temperature" => 0.7, // A bit of creativity for varied responses + "maxOutputTokens" => 150 + ] + ]; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $result = json_decode($response, true); + if (isset($result['candidates'][0]['content']['parts'][0]['text'])) { + return trim($result['candidates'][0]['content']['parts'][0]['text']); + } + } + + return "والله يا صاحبي جربت تطبيق سيرو وارتحت كثير من المشاكل هذي، جربه ما بتندم."; // Fallback response +} + +// Example usage if called directly via CLI: +if (php_sapi_name() === 'cli') { + echo "Testing Gemini Generation...\n"; + $testContext = "التطبيقات الثانية بتاخذ عمولة عالية جداً ومش ملحقين بنزين!"; + $generated = generateCommentWithGemini($testContext, 'promote_siro'); + echo "Generated Comment: \n" . $generated . "\n"; +} diff --git a/backend/bot/social_media_bot/schedule_manager.php b/backend/bot/social_media_bot/schedule_manager.php new file mode 100644 index 00000000..2c526cd6 --- /dev/null +++ b/backend/bot/social_media_bot/schedule_manager.php @@ -0,0 +1,69 @@ +con = Database::get('main'); + } + + /** + * Create a new task and optionally generate a comment for it using Gemini. + * + * @param string $platform 'facebook' or 'instagram' + * @param string $type 'join_group', 'read_posts', 'post_comment', 'share_link' + * @param string|null $targetUrl The group or post URL + * @param string|null $promptContext Context for Gemini + * @param int|null $accountId Specific account ID or null for any available + * @param int $delayMinutes How many minutes to delay this task + */ + public function scheduleTask($platform, $type, $targetUrl = null, $promptContext = null, $accountId = null, $delayMinutes = 0) { + // Quiet hours check (e.g. don't schedule tasks between 1 AM and 6 AM) + $currentHour = (int)date('H'); + if ($currentHour >= 1 && $currentHour < 6) { + // Push it to 6 AM if we are in quiet hours + $hoursToAdd = 6 - $currentHour; + $delayMinutes += ($hoursToAdd * 60); + } + + $scheduledAt = date('Y-m-d H:i:s', strtotime("+$delayMinutes minutes")); + + $generatedComment = null; + if (($type === 'post_comment' || $type === 'share_link') && $promptContext) { + // Pre-generate the comment using Gemini + // If it's share_link, we want an intent to promote, otherwise just answer/interact + $intent = ($type === 'share_link') ? 'promote_siro' : 'answer_question'; + $generatedComment = generateCommentWithGemini($promptContext, $intent); + } + + $stmt = $this->con->prepare(" + INSERT INTO social_tasks (account_id, platform, type, target_url, prompt_context, generated_comment, scheduled_at, status) + VALUES (?, ?, ?, ?, ?, ?, ?, 'pending') + "); + + return $stmt->execute([ + $accountId, + $platform, + $type, + $targetUrl, + $promptContext, + $generatedComment, + $scheduledAt + ]); + } +} + +// Example usage via CLI +if (php_sapi_name() === 'cli' && isset($argv[1]) && $argv[1] === 'test_schedule') { + $sm = new ScheduleManager(); + $context = "شو رأيكم بتطبيق سيرو يا كباتن؟ حد جربه؟"; + $success = $sm->scheduleTask('facebook', 'post_comment', 'https://facebook.com/groups/amman.drivers/post/12345', $context, null, 5); + echo $success ? "Task scheduled successfully!\n" : "Failed to schedule task.\n"; +} diff --git a/backend/bot/social_media_bot/schema.sql b/backend/bot/social_media_bot/schema.sql new file mode 100644 index 00000000..5644bc3a --- /dev/null +++ b/backend/bot/social_media_bot/schema.sql @@ -0,0 +1,43 @@ +-- Social Media Bot Schema + +CREATE TABLE IF NOT EXISTS `social_accounts` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `platform` ENUM('facebook', 'instagram') NOT NULL, + `username` VARCHAR(100) NOT NULL, + `status` ENUM('active', 'restricted', 'banned') DEFAULT 'active', + `total_posts` INT DEFAULT 0, + `total_comments` INT DEFAULT 0, + `last_active` DATETIME NULL, + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `social_tasks` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `account_id` INT NULL, -- NULL if any available account can take it + `platform` ENUM('facebook', 'instagram') NOT NULL, + `type` ENUM('join_group', 'read_posts', 'post_comment', 'share_link') NOT NULL, + `target_url` VARCHAR(500) NULL, -- URL of the group or post + `prompt_context` TEXT NULL, -- Context for Gemini to generate the comment + `generated_comment` TEXT NULL, -- The comment generated by Gemini + `status` ENUM('pending', 'in_progress', 'completed', 'failed') DEFAULT 'pending', + `error_message` TEXT NULL, + `scheduled_at` DATETIME NULL, + `completed_at` DATETIME NULL, + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`account_id`) REFERENCES `social_accounts`(`id`) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS `social_logs` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `task_id` INT NULL, + `account_id` INT NULL, + `log_level` ENUM('info', 'warning', 'error') DEFAULT 'info', + `message` TEXT NOT NULL, + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`task_id`) REFERENCES `social_tasks`(`id`) ON DELETE SET NULL, + FOREIGN KEY (`account_id`) REFERENCES `social_accounts`(`id`) ON DELETE SET NULL +); + +-- Insert dummy account for testing +INSERT IGNORE INTO `social_accounts` (`platform`, `username`, `status`) VALUES ('facebook', 'test_bot_1', 'active'); diff --git a/backend/bot/social_media_bot/social_worker.php b/backend/bot/social_media_bot/social_worker.php new file mode 100644 index 00000000..17d201dd --- /dev/null +++ b/backend/bot/social_media_bot/social_worker.php @@ -0,0 +1,108 @@ + 'error', 'message' => 'Unauthorized'])); +} + +$action = $_GET['action'] ?? ''; + +try { + $con = Database::get('main'); + + switch ($action) { + case 'get_task': + // The bot asks for a task to do + $platform = $_GET['platform'] ?? 'facebook'; + + // Find a pending task that is scheduled for now or earlier + $stmt = $con->prepare(" + SELECT id, type, target_url, prompt_context, generated_comment + FROM social_tasks + WHERE status = 'pending' AND platform = ? AND (scheduled_at IS NULL OR scheduled_at <= NOW()) + ORDER BY created_at ASC LIMIT 1 + "); + $stmt->execute([$platform]); + $task = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($task) { + // Mark as in progress + $update = $con->prepare("UPDATE social_tasks SET status = 'in_progress' WHERE id = ?"); + $update->execute([$task['id']]); + + echo json_encode(['status' => 'success', 'data' => $task]); + } else { + echo json_encode(['status' => 'success', 'message' => 'No tasks available', 'data' => null]); + } + break; + + case 'complete_task': + // The bot reports task completion + $taskId = $_POST['task_id'] ?? null; + $result = $_POST['result'] ?? ''; // e.g. success or error details + + if ($taskId) { + $stmt = $con->prepare("UPDATE social_tasks SET status = 'completed', completed_at = NOW() WHERE id = ?"); + $stmt->execute([$taskId]); + + // Log it + $log = $con->prepare("INSERT INTO social_logs (task_id, log_level, message) VALUES (?, 'info', ?)"); + $log->execute([$taskId, "Task completed: " . substr($result, 0, 200)]); + + echo json_encode(['status' => 'success']); + } else { + echo json_encode(['status' => 'error', 'message' => 'task_id required']); + } + break; + + case 'fail_task': + // The bot reports task failure + $taskId = $_POST['task_id'] ?? null; + $errorMsg = $_POST['error_message'] ?? 'Unknown error'; + + if ($taskId) { + $stmt = $con->prepare("UPDATE social_tasks SET status = 'failed', error_message = ? WHERE id = ?"); + $stmt->execute([$errorMsg, $taskId]); + + // Log it + $log = $con->prepare("INSERT INTO social_logs (task_id, log_level, message) VALUES (?, 'error', ?)"); + $log->execute([$taskId, "Task failed: " . substr($errorMsg, 0, 200)]); + + echo json_encode(['status' => 'success']); + } else { + echo json_encode(['status' => 'error', 'message' => 'task_id required']); + } + break; + + case 'log': + // The bot sends a general log + $accountId = $_POST['account_id'] ?? null; + $level = $_POST['level'] ?? 'info'; + $message = $_POST['message'] ?? ''; + + $log = $con->prepare("INSERT INTO social_logs (account_id, log_level, message) VALUES (?, ?, ?)"); + $log->execute([$accountId, $level, $message]); + + echo json_encode(['status' => 'success']); + break; + + default: + echo json_encode(['status' => 'error', 'message' => 'Invalid action']); + } + +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]); +} diff --git a/backend/ride/location/get.php b/backend/ride/location/get.php index fb756214..7b3de2ed 100644 --- a/backend/ride/location/get.php +++ b/backend/ride/location/get.php @@ -29,10 +29,10 @@ try { } // ========================================== - // 2. طلب الـ IDs والمواقع من سيرفر اللوكيشن (Redis API) + // 2. طلب بيانات السائقين من سيرفر اللوكيشن (Redis API) + // — يرجع cache hits لو موجودة، وإلا يرجع IDs فقط // ========================================== $locationServerUrl = getenv('LOCATION_API_URL'); - // تأكد من المسار الصحيح للمفتاح على السيرفر الرئيسي $INTERNAL_KEY = trim(file_get_contents(getenv('INTERNAL_SOCKET_KEY_PATH'))); $ch = curl_init(); @@ -41,7 +41,7 @@ try { curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ 'lat' => $lat, 'lng' => $lng, - 'radius' => 5, // 5 كم كافية للعرض + 'radius' => 5, 'limit' => 50 ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -65,119 +65,115 @@ try { exit; } - // تجهيز خريطة لدمج البيانات لاحقاً (ID => RedisData) - $driversMap = []; - $driverIds = []; + // تقسيم: cache hits → نستخدمها فوراً، cache misses → نحتاج MySQL + $final_result = []; + $mysqlIds = []; + $driversMap = []; + $serverNow = date('Y-m-d H:i:s'); + foreach ($redisDrivers as $d) { - $driverIds[] = $d['id']; - $driversMap[$d['id']] = $d; - } - - // ========================================== - // 3. جلب التفاصيل الكاملة من MySQL (مثل الملف القديم تماماً) - // ========================================== - - // تجهيز الـ Placeholders - $placeholders = implode(',', array_fill(0, count($driverIds), '?')); - - // الاستعلام الشامل (نفس الحقول القديمة) - $sql_drivers_info = " - SELECT - d.id AS driver_id, - d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus, - cr.make, - cr.car_plate, - cr.model, - cr.color, cr.vin, cr.color_hex, - cr.year, - cr.vehicle_category_id, - dt.token, - COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver - FROM driver d - LEFT JOIN CarRegistration cr ON cr.driverID = d.id - LEFT JOIN driverToken dt ON dt.captain_id = d.id - LEFT JOIN ( - SELECT driver_id, AVG(rating) AS ratingDriver - FROM ratingDriver - GROUP BY driver_id - ) rdAvg ON rdAvg.driver_id = d.id - WHERE d.id IN ($placeholders) - AND COALESCE(cr.year, 0) > 2000 - -- AND (cr.make NOT LIKE '%دراج%' AND cr.model NOT LIKE '%دراج%') - "; - - $stmt = $con->prepare($sql_drivers_info); - $stmt->execute($driverIds); - $drivers_db = $stmt->fetchAll(PDO::FETCH_ASSOC); - - if (empty($drivers_db)) { - jsonSuccess([], "No matching drivers in DB"); - exit; - } - - // ========================================== - // 4. معالجة البيانات، الدمج، وفك التشفير - // ========================================== - - $final_result = []; - $serverNow = date('Y-m-d H:i:s'); - $fieldsToDecrypt = ['phone','email','gender','birthdate','first_name','last_name','token','car_plate','vin']; - - // الهاش الخاص بالإناث (لتحديد النوع لاحقاً إذا لزم الأمر) - // $femaleHash = 'bQ6yWJ2EVXKZooHdGclvmFiDlZCM8UYeO+ILFjDUvpQ='; - - foreach ($drivers_db as $row) { - $did = $row['driver_id']; - - // دمج بيانات الموقع الحية من الريدز (أهم خطوة) - if (isset($driversMap[$did])) { - $redisInfo = $driversMap[$did]; - $row['latitude'] = $redisInfo['lat']; - $row['longitude'] = $redisInfo['lng']; - $row['heading'] = $redisInfo['heading']; - $row['speed'] = $redisInfo['speed']; - // $row['distance'] = $redisInfo['distance']; // إذا أردت إضافتها + if (!empty($d['cached']) && !empty($d['first_name'])) { + // ✅ Cache hit — بيانات السائق كاملة من Redis, نستخدمها مباشرة + $d['serverNow'] = $serverNow; + $final_result[] = $d; } else { - // حالة نادرة: السائق موجود في الاستعلام ولكن ليس في مصفوفة الريدز (لا يجب أن تحدث) - continue; + // ❌ Cache miss — نحتاج نجيب البيانات من MySQL + $mysqlIds[] = $d['id']; + $driversMap[$d['id']] = $d; } + } - $row['serverNow'] = $serverNow; - - // فك التشفير (Decrypt) - foreach ($fieldsToDecrypt as $field) { - if (isset($row[$field]) && $row[$field] !== null && $row[$field] !== '') { - try { - $row[$field] = $encryptionHelper->decryptData($row[$field]); - } catch (Exception $e) { - $row[$field] = null; + // ========================================== + // 3. فقط للسائقين اللي ما فيهم Cache: MySQL + // ========================================== + if (!empty($mysqlIds)) { + $placeholders = implode(',', array_fill(0, count($mysqlIds), '?')); + + $sql_drivers_info = " + SELECT + d.id AS driver_id, + d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus, + cr.make, cr.car_plate, cr.model, cr.color, cr.vin, cr.color_hex, cr.year, cr.vehicle_category_id, + dt.token, + COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver + FROM driver d + LEFT JOIN CarRegistration cr ON cr.driverID = d.id + LEFT JOIN driverToken dt ON dt.captain_id = d.id + LEFT JOIN ( + SELECT driver_id, AVG(rating) AS ratingDriver FROM ratingDriver GROUP BY driver_id + ) rdAvg ON rdAvg.driver_id = d.id + WHERE d.id IN ($placeholders) + AND COALESCE(cr.year, 0) > 2000 + "; + + $stmt = $con->prepare($sql_drivers_info); + $stmt->execute($mysqlIds); + $drivers_db = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $fieldsToDecrypt = ['phone','email','gender','birthdate','first_name','last_name','token','car_plate','vin']; + + foreach ($drivers_db as $row) { + $did = $row['driver_id']; + + if (isset($driversMap[$did])) { + $redisInfo = $driversMap[$did]; + $row['latitude'] = $redisInfo['latitude'] ?? $redisInfo['lat'] ?? ''; + $row['longitude'] = $redisInfo['longitude'] ?? $redisInfo['lng'] ?? ''; + $row['heading'] = $redisInfo['heading'] ?? '0'; + $row['speed'] = $redisInfo['speed'] ?? '0'; + } else { + continue; + } + + $row['serverNow'] = $serverNow; + + foreach ($fieldsToDecrypt as $field) { + if (isset($row[$field]) && $row[$field] !== null && $row[$field] !== '') { + try { + $row[$field] = $encryptionHelper->decryptData($row[$field]); + } catch (Exception $e) { + $row[$field] = null; + } } } + + if (!empty($row['birthdate'])) { + try { + $birthdate = new DateTime($row['birthdate']); + $today = new DateTime(); + $row['age'] = $today->diff($birthdate)->y; + } catch (Exception $e) { $row['age'] = null; } + } else { + $row['age'] = null; + } + + $final_result[] = $row; + + // 🆕 Fill driver:public cache for next time (async, fire-and-forget) + sendToLocationServer('cache_driver_public', [ + 'driver_id' => $did, + 'data' => json_encode([ + 'first_name' => $row['first_name'] ?? '', + 'last_name' => $row['last_name'] ?? '', + 'gender' => $row['gender'] ?? '', + 'make' => $row['make'] ?? '', + 'model' => $row['model'] ?? '', + 'color' => $row['color'] ?? '', + 'color_hex' => $row['color_hex'] ?? '', + 'year' => $row['year'] ?? '', + 'car_plate' => $row['car_plate'] ?? '', + 'ratingDriver'=> $row['ratingDriver'] ?? '0', + 'latitude' => $row['latitude'] ?? '', + 'longitude' => $row['longitude'] ?? '', + 'heading' => $row['heading'] ?? '0', + 'speed' => $row['speed'] ?? '0', + 'updated_at' => time() + ]), + ]); } - - // حساب العمر - if (!empty($row['birthdate'])) { - try { - $birthdate = new DateTime($row['birthdate']); - $today = new DateTime(); - $row['age'] = $today->diff($birthdate)->y; - } catch (Exception $e) { $row['age'] = null; } - } else { - $row['age'] = null; - } - - // إضافة نوع السيارة البسيط (اختياري، إذا كان التطبيق يعتمد عليه) - /* - $type = 'car'; - if ($row['vehicle_category_id'] == 2) $type = 'bike'; - elseif ($row['gender'] == 'female') $type = 'lady'; // بعد فك التشفير تكون female - $row['type'] = $type; - */ - - $final_result[] = $row; } - // إرجاع النتيجة بنفس الهيكل القديم + // إرجاع النتيجة jsonSuccess($final_result); } catch (PDOException $e) { diff --git a/backend/ride/rides/acceptRide.php b/backend/ride/rides/acceptRide.php index c82b619f..83d0329b 100644 --- a/backend/ride/rides/acceptRide.php +++ b/backend/ride/rides/acceptRide.php @@ -199,13 +199,21 @@ try { } // ═══════════════════════════════════════════════════════════ - // STEP F — تنظيف السوق (أبلغ location server إن الرحلة محجوزة) + // STEP F — تنظيف السوق + Cache ride state (أبلغ location server) // ═══════════════════════════════════════════════════════════ sendToLocationServer('ride_taken_event', [ 'ride_id' => $rideId, 'taken_by_driver_id' => $driverId, ]); + // 🆕 Cache ride state in Redis + sendToLocationServer('update_ride_state', [ + 'ride_id' => $rideId, + 'status' => $status, + 'driver_id' => $driverId, + 'passenger_id' => $passengerIdValue ?? '', + ]); + error_log("[accept_ride] SUCCESS. RideID=$rideId accepted by DriverID=$driverId"); // ═══════════════════════════════════════════════════════════ diff --git a/backend/ride/rides/arrive_ride.php b/backend/ride/rides/arrive_ride.php index 58ee7896..b900c764 100644 --- a/backend/ride/rides/arrive_ride.php +++ b/backend/ride/rides/arrive_ride.php @@ -67,6 +67,14 @@ try { } } + // 🆕 Cache ride state in Redis + sendToLocationServer('update_ride_state', [ + 'ride_id' => $rideId, + 'status' => 'arrived', + 'driver_id' => $driverId, + 'passenger_id' => $passenger_id ?? '', + ]); + jsonSuccess(null, "Arrival notified successfully"); } catch (Exception $e) { diff --git a/backend/ride/rides/cancel_ride_by_driver.php b/backend/ride/rides/cancel_ride_by_driver.php index c950a23d..f06623cc 100644 --- a/backend/ride/rides/cancel_ride_by_driver.php +++ b/backend/ride/rides/cancel_ride_by_driver.php @@ -212,6 +212,14 @@ try { $con->commit(); + // 🆕 Cache ride state in Redis + sendToLocationServer('update_ride_state', [ + 'ride_id' => $rideId, + 'status' => 'cancelled_by_driver', + 'driver_id' => $driverId, + 'passenger_id' => $passenger_id ?? '', + ]); + // 5. الرد للفلاتر echo json_encode([ "status" => "success", diff --git a/backend/ride/rides/cancel_ride_by_passenger.php b/backend/ride/rides/cancel_ride_by_passenger.php index 566f6626..0c779ca5 100644 --- a/backend/ride/rides/cancel_ride_by_passenger.php +++ b/backend/ride/rides/cancel_ride_by_passenger.php @@ -146,6 +146,14 @@ try { } } + // 🆕 Cache ride state in Redis + sendToLocationServer('update_ride_state', [ + 'ride_id' => $rideId, + 'status' => 'cancelled_by_passenger', + 'driver_id' => $driverId ?? '', + 'passenger_id' => $ride['passenger_id'] ?? '', + ]); + jsonSuccess(null, "Ride cancelled successfully"); } catch (PDOException $e) { diff --git a/backend/ride/rides/start_ride.php b/backend/ride/rides/start_ride.php index b0a3c7a9..aff4cc58 100644 --- a/backend/ride/rides/start_ride.php +++ b/backend/ride/rides/start_ride.php @@ -117,6 +117,15 @@ try { } $con->commit(); + + // 🆕 Cache ride state in Redis + sendToLocationServer('update_ride_state', [ + 'ride_id' => $ride_id, + 'status' => 'started', + 'driver_id' => $driver_id, + 'passenger_id' => $passenger_id ?? '', + ]); + jsonSuccess(null, "Ride started successfully"); } catch (PDOException $e) { diff --git a/loction_server/api_get_nearby.php b/loction_server/api_get_nearby.php index 03e38365..2c82f966 100755 --- a/loction_server/api_get_nearby.php +++ b/loction_server/api_get_nearby.php @@ -61,13 +61,24 @@ try { continue; } - $validDrivers[] = [ - 'id' => $d_id, - 'distance' => $d_dist, - 'heading' => $profile['heading'] ?? 0, - 'speed' => $profile['speed'] ?? 0, - 'lat' => $d_coord[1], // Latitude من الريدز (أدق) - 'lng' => $d_coord[0] // Longitude من الريدز + // 🆕 فحص driver:public cache (بيانات السائق الثابتة) + $public = $redis->hgetall("driver:public:$d_id"); + $hasCache = !empty($public) && !empty($public['first_name']); + + $validDrivers[] = $hasCache ? array_merge($public, [ + 'id' => $d_id, + 'driver_id' => $d_id, + 'distance' => $d_dist, + 'cached' => true + ]) : [ + 'id' => $d_id, + 'driver_id' => $d_id, + 'distance' => $d_dist, + 'heading' => $profile['heading'] ?? 0, + 'speed' => $profile['speed'] ?? 0, + 'latitude' => $d_coord[1], + 'longitude' => $d_coord[0], + 'cached' => false ]; if (count($validDrivers) >= $limit) break; diff --git a/loction_server/driver_socket.php b/loction_server/driver_socket.php index 74c150c1..4165a9b1 100755 --- a/loction_server/driver_socket.php +++ b/loction_server/driver_socket.php @@ -230,6 +230,10 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) { if (isset($ops['hmset'])) { $pipe->hmset($profileKey, $ops['hmset']); + // 🆕 Cache public driver data للقراءة السريعة من get.php (24h TTL) + $publicKey = "driver:public:$driverId"; + $pipe->hmset($publicKey, $ops['hmset']); + $pipe->expire($publicKey, 86400); } if (isset($ops['expire'])) { $pipe->expire($profileKey, $ops['expire']); @@ -418,6 +422,34 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) { logMsg("✅ Ride #$rideId taken by #$winnerDriverId."); $connection->send('OK'); + // ── 7. Update Ride State (Redis Cache فقط — بدون Forward) + } elseif ($action === 'update_ride_state') { + $rideId = $post['ride_id'] ?? null; + $status = $post['status'] ?? ''; + $driverId = $post['driver_id'] ?? ''; + $passengerId = $post['passenger_id'] ?? ''; + + if (!$rideId || !$status) { + $connection->send('Error: Missing ride_id or status'); + return; + } + + if ($redis) { + $stateKey = "ride:$rideId:state"; + $stateData = [ + 'status' => $status, + 'driver_id' => $driverId, + 'passenger_id' => $passengerId, + 'updated_at' => time(), + ]; + $redis->hmset($stateKey, $stateData); + $redis->expire($stateKey, 86400); + + logMsg("🚗 Ride #$rideId → status: $status (cached in Redis)"); + } + + $connection->send('OK'); + // ── 5. Force Disconnect ─────────────────────────────── } elseif ($action === 'force_disconnect') { $driverId = $post['driver_id'] ?? null; @@ -467,6 +499,20 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) { } $connection->send('OK'); + // ── 7. Cache Driver Public Data ───────────────────────── + } elseif ($action === 'cache_driver_public') { + $driverId = $post['driver_id'] ?? null; + $data = json_decode($post['data'] ?? '[]', true); + + if ($driverId && $redis && !empty($data)) { + $key = "driver:public:$driverId"; + $redis->hmset($key, $data); + $redis->expire($key, 86400); + $connection->send('OK'); + } else { + $connection->send('Error'); + } + } else { $connection->send('Unknown action'); } @@ -631,7 +677,9 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) { if ($needHmset) { $eventBuffer[$driverId]['hmset'] = [ - 'id' => $driverId, 'heading' => $heading, 'speed' => $speed, 'status' => $status, 'updated_at' => $now + 'id' => $driverId, 'lat' => $lat, 'lng' => $lng, + 'heading' => $heading, 'speed' => $speed, + 'status' => $status, 'updated_at' => $now ]; $state['speed'] = $speedMs; $state['heading'] = $heading; @@ -667,6 +715,13 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) { if ($needGeoadd) { $state['lat'] = $lat; $state['lng'] = $lng; + // 🆕 تحديث الموقع في driver:public (حتى لو ما تغير speed/heading) + if (!isset($eventBuffer[$driverId]['hmset'])) { + $eventBuffer[$driverId]['hmset'] = [ + 'id' => $driverId, 'lat' => $lat, 'lng' => $lng, + 'updated_at' => $now + ]; + } } } }); diff --git a/siro_driver/lib/constant/links.dart b/siro_driver/lib/constant/links.dart index e55a4663..2207db95 100755 --- a/siro_driver/lib/constant/links.dart +++ b/siro_driver/lib/constant/links.dart @@ -127,7 +127,7 @@ class AppLink { static String get server => endPoint; ///=================ride==========================/// - ///https://api.intaleq.xyz/siro/ride + ///https://jordan-siro.intaleqapp.com/backend static String get ride => '$server/ride'; static String get rideServer { switch (currentCountry) { diff --git a/siro_driver/lib/controller/functions/background_service.dart b/siro_driver/lib/controller/functions/background_service.dart index 4045c0af..bc9286fd 100644 --- a/siro_driver/lib/controller/functions/background_service.dart +++ b/siro_driver/lib/controller/functions/background_service.dart @@ -51,7 +51,7 @@ Future onStart(ServiceInstance service) async { print("✅ Background Service: Socket Connected! ID: ${socket?.id}"); // 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الخدمة - socket.emit('get_pending_orders'); + socket!.emit('get_pending_orders'); if (service is AndroidServiceInstance) { flutterLocalNotificationsPlugin.show( diff --git a/socialBot/.gitignore b/socialBot/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/socialBot/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/socialBot/app/.gitignore b/socialBot/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/socialBot/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/socialBot/app/build.gradle.kts b/socialBot/app/build.gradle.kts new file mode 100644 index 00000000..b59ff402 --- /dev/null +++ b/socialBot/app/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.siro.socialmedia_bot" + compileSdk = 36 + + defaultConfig { + applicationId = "com.siro.socialmedia_bot" + minSdk = 21 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + + // Coroutines – required by the bot services + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/socialBot/app/proguard-rules.pro b/socialBot/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/socialBot/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/socialBot/app/src/androidTest/java/com/siro/socialmedia_bot/ExampleInstrumentedTest.kt b/socialBot/app/src/androidTest/java/com/siro/socialmedia_bot/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..80701a6a --- /dev/null +++ b/socialBot/app/src/androidTest/java/com/siro/socialmedia_bot/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.siro.socialmedia_bot + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.siro.socialmedia_bot", appContext.packageName) + } +} \ No newline at end of file diff --git a/socialBot/app/src/main/AndroidManifest.xml b/socialBot/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..10711e71 --- /dev/null +++ b/socialBot/app/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/MainActivity.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/MainActivity.kt new file mode 100644 index 00000000..f4c8cbe9 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/MainActivity.kt @@ -0,0 +1,93 @@ +package com.siro.socialmedia_bot + +import android.accessibilityservice.AccessibilityServiceInfo +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import android.view.accessibility.AccessibilityManager +import androidx.appcompat.app.AppCompatActivity +import com.siro.socialmedia_bot.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + private val logBuffer = StringBuilder() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.btnEnableAccessibility.setOnClickListener { + openAccessibilitySettings() + } + + appendLog("[System] Siro Social Media Bot started.") + appendLog("[System] Checking service status...") + } + + override fun onResume() { + super.onResume() + updateServiceStatus() + } + + private fun updateServiceStatus() { + val isFbEnabled = isAccessibilityServiceEnabled("com.siro.socialmedia_bot/.social.facebook.FacebookBotService") + val isIgEnabled = isAccessibilityServiceEnabled("com.siro.socialmedia_bot/.social.instagram.InstagramBotService") + + if (isFbEnabled || isIgEnabled) { + binding.statusDot.backgroundTintList = getColorStateList(android.R.color.holo_green_light) + binding.tvStatus.text = "Accessibility Service Active" + binding.btnEnableAccessibility.text = "✓ Service is Running" + binding.btnEnableAccessibility.backgroundTintList = getColorStateList(android.R.color.holo_green_dark) + } else { + binding.statusDot.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFFFF4444.toInt()) + binding.tvStatus.text = "Accessibility Service Disabled" + binding.btnEnableAccessibility.text = "Enable Accessibility Service" + binding.btnEnableAccessibility.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFF1565C0.toInt()) + } + + // Facebook dot + if (isFbEnabled) { + binding.fbDot.backgroundTintList = getColorStateList(android.R.color.holo_green_light) + binding.tvFbState.text = "RUNNING" + binding.tvFbState.setTextColor(0xFF00FF88.toInt()) + appendLog("[Facebook] Service is active and polling for tasks.") + } else { + binding.fbDot.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFF555555.toInt()) + binding.tvFbState.text = "DISABLED" + binding.tvFbState.setTextColor(0xFF555555.toInt()) + } + + // Instagram dot + if (isIgEnabled) { + binding.igDot.backgroundTintList = getColorStateList(android.R.color.holo_green_light) + binding.tvIgState.text = "RUNNING" + binding.tvIgState.setTextColor(0xFF00FF88.toInt()) + appendLog("[Instagram] Service is active and polling for tasks.") + } else { + binding.igDot.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFF555555.toInt()) + binding.tvIgState.text = "DISABLED" + binding.tvIgState.setTextColor(0xFF555555.toInt()) + } + } + + private fun isAccessibilityServiceEnabled(serviceId: String): Boolean { + val am = getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager + val enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK) + return enabledServices.any { it.id == serviceId } + } + + private fun openAccessibilitySettings() { + val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) + startActivity(intent) + appendLog("[System] Opened Accessibility settings. Enable both 'Siro Facebook Bot' and 'Siro Instagram Bot'.") + } + + private fun appendLog(msg: String) { + val timestamp = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date()) + logBuffer.append("[$timestamp] $msg\n") + binding.tvLog.text = logBuffer.toString() + binding.scrollLog.post { binding.scrollLog.fullScroll(android.view.View.FOCUS_DOWN) } + } +} diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/network/SocialBotClient.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/network/SocialBotClient.kt new file mode 100644 index 00000000..301e8b56 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/network/SocialBotClient.kt @@ -0,0 +1,72 @@ +package com.siro.socialmedia_bot.network + +import org.json.JSONObject +import java.io.OutputStreamWriter +import java.net.HttpURLConnection +import java.net.URL +import java.util.Scanner + +/** + * Client to connect to the social_worker.php endpoint + */ +object SocialBotClient { + private const val BASE_URL = "https://your-domain.com/backend/bot/social_media_bot/social_worker.php" + private const val BOT_TOKEN = "YOUR_SECRET_BOT_TOKEN" + + fun getTask(platform: String): JSONObject? { + try { + val url = URL("$BASE_URL?action=get_task&platform=$platform") + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.setRequestProperty("X-Bot-Token", BOT_TOKEN) + + val responseCode = connection.responseCode + if (responseCode == 200) { + val scanner = Scanner(connection.inputStream) + val response = scanner.useDelimiter("\\A").next() + scanner.close() + + val json = JSONObject(response) + if (json.getString("status") == "success" && !json.isNull("data")) { + return json.getJSONObject("data") + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return null + } + + fun completeTask(taskId: Int, result: String) { + postData("action=complete_task", "task_id=$taskId&result=$result") + } + + fun failTask(taskId: Int, errorMessage: String) { + postData("action=fail_task", "task_id=$taskId&error_message=$errorMessage") + } + + fun logMessage(accountId: Int?, level: String, message: String) { + val accIdParam = accountId?.let { "&account_id=$it" } ?: "" + postData("action=log", "level=$level&message=$message$accIdParam") + } + + private fun postData(actionParam: String, postBody: String) { + try { + val url = URL("$BASE_URL?$actionParam") + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.setRequestProperty("X-Bot-Token", BOT_TOKEN) + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded") + connection.doOutput = true + + val writer = OutputStreamWriter(connection.outputStream) + writer.write(postBody) + writer.flush() + writer.close() + + connection.responseCode // execute + } catch (e: Exception) { + e.printStackTrace() + } + } +} diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookBotService.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookBotService.kt new file mode 100644 index 00000000..7a19f902 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookBotService.kt @@ -0,0 +1,115 @@ +package com.siro.socialmedia_bot.social.facebook + +import android.accessibilityservice.AccessibilityService +import android.view.accessibility.AccessibilityEvent +import android.util.Log +import com.siro.socialmedia_bot.network.SocialBotClient +import com.siro.socialmedia_bot.social.model.SocialTask +import kotlinx.coroutines.* + +class FacebookBotService : AccessibilityService() { + + private val TAG = "FacebookBotService" + private val scope = CoroutineScope(Dispatchers.IO + Job()) + private var isBotRunning = false + private var currentTask: SocialTask? = null + + private lateinit var navigator: FacebookNavigator + private lateinit var commentReader: FacebookCommentReader + private lateinit var commentPoster: FacebookCommentPoster + + override fun onServiceConnected() { + super.onServiceConnected() + Log.d(TAG, "Facebook Accessibility Service Connected") + + navigator = FacebookNavigator(this) + commentReader = FacebookCommentReader(this) + commentPoster = FacebookCommentPoster(this) + + startTaskLoop() + } + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + // We handle logic manually via coroutines and window inspection, + // but we can listen to events if needed for synchronization. + } + + override fun onInterrupt() { + Log.d(TAG, "Service Interrupted") + isBotRunning = false + } + + override fun onDestroy() { + super.onDestroy() + scope.cancel() + } + + private fun startTaskLoop() { + if (isBotRunning) return + isBotRunning = true + + scope.launch { + while (isBotRunning) { + try { + // Check for tasks + val taskData = SocialBotClient.getTask("facebook") + if (taskData != null) { + currentTask = SocialTask( + id = taskData.getInt("id"), + type = taskData.getString("type"), + targetUrl = if (taskData.isNull("target_url")) null else taskData.getString("target_url"), + promptContext = if (taskData.isNull("prompt_context")) null else taskData.getString("prompt_context"), + generatedComment = if (taskData.isNull("generated_comment")) null else taskData.getString("generated_comment") + ) + + Log.d(TAG, "Received Task: ${currentTask?.type}") + executeTask(currentTask!!) + } else { + Log.d(TAG, "No tasks available. Sleeping...") + delay(60000) // Wait 1 minute before checking again + } + } catch (e: Exception) { + Log.e(TAG, "Error in task loop", e) + delay(30000) // Wait 30 seconds on error + } + } + } + } + + private suspend fun executeTask(task: SocialTask) { + try { + when (task.type) { + "post_comment" -> { + task.targetUrl?.let { url -> + navigator.openUrl(url) + delay(5000) // Wait for page load + + task.generatedComment?.let { comment -> + val success = commentPoster.postComment(comment) + if (success) { + SocialBotClient.completeTask(task.id, "Comment posted successfully") + } else { + SocialBotClient.failTask(task.id, "Failed to find comment box or post") + } + } + } + } + "read_posts" -> { + // Logic to read posts and send to backend + // ... + SocialBotClient.completeTask(task.id, "Posts read successfully") + } + else -> { + Log.w(TAG, "Unknown task type: ${task.type}") + SocialBotClient.failTask(task.id, "Unknown task type") + } + } + } catch (e: Exception) { + SocialBotClient.failTask(task.id, "Exception during execution: ${e.message}") + } + + // Add human-like delay between tasks + val randomDelay = (10000..30000).random().toLong() + delay(randomDelay) + } +} diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentPoster.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentPoster.kt new file mode 100644 index 00000000..68404d97 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentPoster.kt @@ -0,0 +1,76 @@ +package com.siro.socialmedia_bot.social.facebook + +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.AccessibilityService.GestureResultCallback +import android.accessibilityservice.GestureDescription +import android.graphics.Path +import android.os.Bundle +import android.view.accessibility.AccessibilityNodeInfo +import kotlinx.coroutines.delay + +class FacebookCommentPoster(private val service: AccessibilityService) { + + suspend fun postComment(commentText: String): Boolean { + val root = service.rootInActiveWindow ?: return false + + // 1. Find the comment input box. + // In Facebook, it might have contentDescription like "Write a comment..." or similar. + val inputNode = findNodeByContentDescription(root, "Write a comment") + ?: findNodeByClassName(root, "android.widget.EditText") + + if (inputNode == null) return false + + // 2. Click the input node to focus it + inputNode.performAction(AccessibilityNodeInfo.ACTION_CLICK) + delay(1000) // wait for keyboard + + // 3. Set text (simulating human typing can be done by pasting chunks or using set_text) + val arguments = Bundle() + arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, commentText) + inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments) + + // Wait a bit to simulate human typing delay + val randomTypingDelay = (commentText.length * 50).toLong().coerceAtMost(5000L) + delay(randomTypingDelay) + + // 4. Find and click the send button + // Often has a content description like "Send" or it's an ImageView next to EditText + val sendBtn = findNodeByContentDescription(service.rootInActiveWindow, "Send") + if (sendBtn != null) { + sendBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK) + delay(2000) + return true + } + + return false + } + + private fun findNodeByContentDescription(node: AccessibilityNodeInfo?, descPrefix: String): AccessibilityNodeInfo? { + if (node == null) return null + + if (node.contentDescription?.startsWith(descPrefix, ignoreCase = true) == true || + node.text?.startsWith(descPrefix, ignoreCase = true) == true) { + return node + } + + for (i in 0 until node.childCount) { + val child = findNodeByContentDescription(node.getChild(i), descPrefix) + if (child != null) return child + } + return null + } + + private fun findNodeByClassName(node: AccessibilityNodeInfo?, className: String): AccessibilityNodeInfo? { + if (node == null) return null + + if (node.className?.toString() == className) { + return node + } + + for (i in 0 until node.childCount) { + val child = findNodeByClassName(node.getChild(i), className) + if (child != null) return child + } + return null + } +} diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentReader.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentReader.kt new file mode 100644 index 00000000..8e2814b8 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookCommentReader.kt @@ -0,0 +1,34 @@ +package com.siro.socialmedia_bot.social.facebook + +import android.accessibilityservice.AccessibilityService +import android.view.accessibility.AccessibilityNodeInfo + +class FacebookCommentReader(private val service: AccessibilityService) { + + fun extractComments(): List { + val root = service.rootInActiveWindow ?: return emptyList() + val comments = mutableListOf() + + // Facebook UI is complex and changes often. + // This is a simplified logic. We'd usually look for nodes with text content inside a RecyclerView + findTextNodes(root, comments) + + return comments + } + + private fun findTextNodes(node: AccessibilityNodeInfo?, comments: MutableList) { + if (node == null) return + + if (node.text != null && node.text.isNotEmpty()) { + // Add filtering logic to ensure it's a comment and not just UI text + val text = node.text.toString() + if (text.length > 10) { // arbitrary filter + comments.add(text) + } + } + + for (i in 0 until node.childCount) { + findTextNodes(node.getChild(i), comments) + } + } +} diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookNavigator.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookNavigator.kt new file mode 100644 index 00000000..ed554dcb --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/facebook/FacebookNavigator.kt @@ -0,0 +1,26 @@ +package com.siro.socialmedia_bot.social.facebook + +import android.accessibilityservice.AccessibilityService +import android.content.Intent +import android.net.Uri +import kotlinx.coroutines.delay + +class FacebookNavigator(private val service: AccessibilityService) { + + suspend fun openUrl(url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setPackage("com.facebook.katana") // Open specifically in Facebook app + + try { + service.startActivity(intent) + } catch (e: Exception) { + // Fallback if Facebook app is not installed + intent.setPackage(null) + service.startActivity(intent) + } + + // Wait for app to open + delay(3000) + } +} diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/instagram/InstagramBotService.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/instagram/InstagramBotService.kt new file mode 100644 index 00000000..8343ea13 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/instagram/InstagramBotService.kt @@ -0,0 +1,100 @@ +package com.siro.socialmedia_bot.social.instagram + +import android.accessibilityservice.AccessibilityService +import android.view.accessibility.AccessibilityEvent +import android.util.Log +import com.siro.socialmedia_bot.network.SocialBotClient +import com.siro.socialmedia_bot.social.model.SocialTask +import kotlinx.coroutines.* + +/** + * Instagram Accessibility Service – mirrors the Facebook bot architecture + * but targets the Instagram package (com.instagram.android). + */ +class InstagramBotService : AccessibilityService() { + + private val TAG = "InstagramBotService" + private val scope = CoroutineScope(Dispatchers.IO + Job()) + private var isBotRunning = false + + private lateinit var navigator: InstagramNavigator + + override fun onServiceConnected() { + super.onServiceConnected() + Log.d(TAG, "Instagram Accessibility Service Connected") + navigator = InstagramNavigator(this) + startTaskLoop() + } + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + // Observe events when needed for synchronization + } + + override fun onInterrupt() { + Log.d(TAG, "Service Interrupted") + isBotRunning = false + } + + override fun onDestroy() { + super.onDestroy() + scope.cancel() + } + + private fun startTaskLoop() { + if (isBotRunning) return + isBotRunning = true + + scope.launch { + while (isBotRunning) { + try { + val taskData = SocialBotClient.getTask("instagram") + if (taskData != null) { + val task = SocialTask( + id = taskData.getInt("id"), + type = taskData.getString("type"), + targetUrl = if (taskData.isNull("target_url")) null else taskData.getString("target_url"), + promptContext = if (taskData.isNull("prompt_context")) null else taskData.getString("prompt_context"), + generatedComment = if (taskData.isNull("generated_comment")) null else taskData.getString("generated_comment") + ) + Log.d(TAG, "Received Task: ${task.type}") + executeTask(task) + } else { + Log.d(TAG, "No Instagram tasks. Sleeping...") + delay(60_000) + } + } catch (e: Exception) { + Log.e(TAG, "Error in task loop", e) + delay(30_000) + } + } + } + } + + private suspend fun executeTask(task: SocialTask) { + try { + when (task.type) { + "post_comment" -> { + task.targetUrl?.let { url -> + navigator.openUrl(url) + delay(5_000) + task.generatedComment?.let { comment -> + val success = navigator.postComment(comment) + if (success) { + SocialBotClient.completeTask(task.id, "IG comment posted") + } else { + SocialBotClient.failTask(task.id, "Could not find IG comment input") + } + } + } + } + else -> { + Log.w(TAG, "Unknown task type: ${task.type}") + SocialBotClient.failTask(task.id, "Unknown task type") + } + } + } catch (e: Exception) { + SocialBotClient.failTask(task.id, "Exception: ${e.message}") + } + delay((15_000..45_000).random().toLong()) // human-like cooldown + } +} diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/instagram/InstagramNavigator.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/instagram/InstagramNavigator.kt new file mode 100644 index 00000000..acdaa2d6 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/instagram/InstagramNavigator.kt @@ -0,0 +1,89 @@ +package com.siro.socialmedia_bot.social.instagram + +import android.accessibilityservice.AccessibilityService +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.accessibility.AccessibilityNodeInfo +import kotlinx.coroutines.delay + +/** + * Handles navigation and interaction within the Instagram app. + * Instagram's package name: com.instagram.android + */ +class InstagramNavigator(private val service: AccessibilityService) { + + suspend fun openUrl(url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setPackage("com.instagram.android") + + try { + service.startActivity(intent) + } catch (e: Exception) { + intent.setPackage(null) + service.startActivity(intent) + } + delay(4_000) // Wait for Instagram to load + } + + suspend fun postComment(commentText: String): Boolean { + val root = service.rootInActiveWindow ?: return false + + // On Instagram the comment field usually has a hint "Add a comment…" + val inputNode = findNodeByHint(root, "Add a comment") + ?: findNodeByClassName(root, "android.widget.EditText") + ?: return false + + inputNode.performAction(AccessibilityNodeInfo.ACTION_CLICK) + delay(1_000) + + val args = Bundle() + args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, commentText) + inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args) + + delay((commentText.length * 40L).coerceAtMost(4_000L)) // typing simulation + + // Look for "Post" button + val postBtn = findNodeByText(service.rootInActiveWindow, "Post") + if (postBtn != null) { + postBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK) + delay(2_000) + return true + } + return false + } + + // ─── Utility helpers ─────────────────────────────────────────────────────── + + private fun findNodeByHint(node: AccessibilityNodeInfo?, hint: String): AccessibilityNodeInfo? { + if (node == null) return null + if (node.hintText?.contains(hint, ignoreCase = true) == true) return node + for (i in 0 until node.childCount) { + val found = findNodeByHint(node.getChild(i), hint) + if (found != null) return found + } + return null + } + + private fun findNodeByText(node: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? { + if (node == null) return null + if (node.text?.equals(text, ignoreCase = true) == true || + node.contentDescription?.equals(text, ignoreCase = true) == true) return node + for (i in 0 until node.childCount) { + val found = findNodeByText(node.getChild(i), text) + if (found != null) return found + } + return null + } + + private fun findNodeByClassName(node: AccessibilityNodeInfo?, className: String): AccessibilityNodeInfo? { + if (node == null) return null + if (node.className?.toString() == className) return node + for (i in 0 until node.childCount) { + val found = findNodeByClassName(node.getChild(i), className) + if (found != null) return found + } + return null + } +} diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/Account.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/Account.kt new file mode 100644 index 00000000..8989c8d6 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/Account.kt @@ -0,0 +1,7 @@ +package com.siro.socialmedia_bot.social.model + +data class Account( + val id: Int, + val platform: String, + val username: String +) diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/Comment.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/Comment.kt new file mode 100644 index 00000000..278172b1 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/Comment.kt @@ -0,0 +1,7 @@ +package com.siro.socialmedia_bot.social.model + +data class Comment( + val author: String, + val content: String, + val time: String +) diff --git a/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/SocialTask.kt b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/SocialTask.kt new file mode 100644 index 00000000..6463ba00 --- /dev/null +++ b/socialBot/app/src/main/java/com/siro/socialmedia_bot/social/model/SocialTask.kt @@ -0,0 +1,9 @@ +package com.siro.socialmedia_bot.social.model + +data class SocialTask( + val id: Int, + val type: String, // 'join_group', 'read_posts', 'post_comment', 'share_link' + val targetUrl: String?, + val promptContext: String?, + val generatedComment: String? +) diff --git a/socialBot/app/src/main/res/drawable/circle_indicator.xml b/socialBot/app/src/main/res/drawable/circle_indicator.xml new file mode 100644 index 00000000..9f3c01df --- /dev/null +++ b/socialBot/app/src/main/res/drawable/circle_indicator.xml @@ -0,0 +1,5 @@ + + + + diff --git a/socialBot/app/src/main/res/drawable/ic_launcher_background.xml b/socialBot/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/socialBot/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/socialBot/app/src/main/res/drawable/ic_launcher_foreground.xml b/socialBot/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/socialBot/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/socialBot/app/src/main/res/layout/activity_main.xml b/socialBot/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..edd00486 --- /dev/null +++ b/socialBot/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +