diff --git a/backend/bot/social_media_bot/schema.sql b/backend/bot/social_media_bot/schema.sql deleted file mode 100644 index 5644bc3a..00000000 --- a/backend/bot/social_media_bot/schema.sql +++ /dev/null @@ -1,43 +0,0 @@ --- 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/account_manager.php b/backend/marketing_engine/account_manager.php similarity index 100% rename from backend/bot/social_media_bot/account_manager.php rename to backend/marketing_engine/account_manager.php diff --git a/backend/bot/social_media_bot/gemini_comment_generator.php b/backend/marketing_engine/gemini_comment_generator.php similarity index 100% rename from backend/bot/social_media_bot/gemini_comment_generator.php rename to backend/marketing_engine/gemini_comment_generator.php diff --git a/backend/marketing_engine/index.php b/backend/marketing_engine/index.php new file mode 100644 index 00000000..206cfce1 --- /dev/null +++ b/backend/marketing_engine/index.php @@ -0,0 +1,88 @@ + 'error', 'message' => 'Unauthorized'])); +} + +$action = $_GET['action'] ?? ''; +$platform = $_GET['platform'] ?? 'tiktok'; // Default to tiktok for new platform testing + +try { + // In a real scenario, you'd use a dedicated database. + // Here we use the main DB but with marketing_ prefixed tables. + $con = Database::get('main'); + + switch ($action) { + case 'get_task': + // 1. First, check if the bot needs a task + // We look for pending tasks for this platform + $stmt = $con->prepare(" + SELECT id, type, target_url, content_text, media_url + FROM marketing_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) { + // If the task requires a media file (like upload_video) + // The Android app will need to download it first using the media_url + + // Mark as in progress + $update = $con->prepare("UPDATE marketing_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': + $taskId = $_POST['task_id'] ?? null; + $result = $_POST['result'] ?? ''; + + if ($taskId) { + $stmt = $con->prepare("UPDATE marketing_tasks SET status = 'completed', completed_at = NOW() WHERE id = ?"); + $stmt->execute([$taskId]); + echo json_encode(['status' => 'success']); + } else { + echo json_encode(['status' => 'error', 'message' => 'task_id required']); + } + break; + + case 'fail_task': + $taskId = $_POST['task_id'] ?? null; + $errorMsg = $_POST['error_message'] ?? 'Unknown error'; + + if ($taskId) { + $stmt = $con->prepare("UPDATE marketing_tasks SET status = 'failed', error_message = ? WHERE id = ?"); + $stmt->execute([$errorMsg, $taskId]); + echo json_encode(['status' => 'success']); + } else { + echo json_encode(['status' => 'error', 'message' => 'task_id required']); + } + break; + + default: + echo json_encode(['status' => 'error', 'message' => 'Invalid action']); + } + +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['status' => 'error', 'message' => 'Server error: ' . $e->getMessage()]); +} diff --git a/backend/marketing_engine/insert_test_task.php b/backend/marketing_engine/insert_test_task.php new file mode 100644 index 00000000..29d22a22 --- /dev/null +++ b/backend/marketing_engine/insert_test_task.php @@ -0,0 +1,11 @@ +query("INSERT INTO marketing_tasks (platform, type, status) VALUES ('facebook', 'read_posts', 'pending')"); + +// Also insert into social_tasks just in case they hit the old endpoint +$con->query("INSERT INTO social_tasks (platform, type, status) VALUES ('facebook', 'read_posts', 'pending')"); + +echo "Test task inserted successfully!\n"; diff --git a/backend/bot/social_media_bot/schedule_manager.php b/backend/marketing_engine/schedule_manager.php similarity index 100% rename from backend/bot/social_media_bot/schedule_manager.php rename to backend/marketing_engine/schedule_manager.php diff --git a/backend/marketing_engine/schema.sql b/backend/marketing_engine/schema.sql new file mode 100644 index 00000000..49b57e78 --- /dev/null +++ b/backend/marketing_engine/schema.sql @@ -0,0 +1,70 @@ +-- ========================================================== +-- Marketing Engine & Social Media Bot Combined Schema +-- ========================================================== + +-- 1. Original Social Media Tables (Facebook, Instagram) +CREATE TABLE IF NOT EXISTS `social_accounts` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `platform` ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube') NOT NULL, + `username` VARCHAR(100) NOT NULL, + `status` ENUM('active', 'restricted', 'banned') DEFAULT 'active', + `total_posts` INT DEFAULT 0, + `total_comments` INT DEFAULT 0, + `total_videos` 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, + `platform` ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube') NOT NULL, + `type` ENUM('join_group', 'read_posts', 'post_comment', 'share_link', 'upload_video', 'post_tweet') NOT NULL, + `target_url` VARCHAR(500) NULL, -- URL of the group, post, or media download + `prompt_context` TEXT NULL, + `generated_comment` TEXT NULL, + `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 +); + +-- 2. New Content Generation Pipeline Tables +CREATE TABLE IF NOT EXISTS `content_pipeline` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `topic` VARCHAR(255) NOT NULL, + `script_text` TEXT NULL, + `voice_url` VARCHAR(500) NULL, + `video_url` VARCHAR(500) NULL, + `status` ENUM('pending', 'script_generated', 'voice_generated', 'video_rendered', 'published', 'failed') DEFAULT 'pending', + `error_message` TEXT NULL, + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `api_quotas` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `service_name` ENUM('gemini', 'elevenlabs', 'creatomate', 'heygen') NOT NULL, + `daily_usage` INT DEFAULT 0, + `quota_limit` INT DEFAULT 10, + `last_reset` DATE NOT NULL +); + +INSERT IGNORE INTO `api_quotas` (`service_name`, `daily_usage`, `quota_limit`, `last_reset`) VALUES +('gemini', 0, 100, CURDATE()), +('elevenlabs', 0, 5, CURDATE()), +('creatomate', 0, 2, CURDATE()); diff --git a/backend/marketing_engine/services/AIVideoGenerator.php b/backend/marketing_engine/services/AIVideoGenerator.php new file mode 100644 index 00000000..b9444189 --- /dev/null +++ b/backend/marketing_engine/services/AIVideoGenerator.php @@ -0,0 +1,59 @@ +elevenLabsApiKey = getenv('ELEVENLABS_API_KEY'); + $this->creatomateApiKey = getenv('CREATOMATE_API_KEY'); + $this->geminiApiKey = getenv('GEMINI_API_KEY'); + } + // Abstracted text generation + public function generateScript($topic) { + // Here you would call Gemini API to generate a video script about the topic + // For example: "Write a 30 second TikTok script about how ride hailing drivers lose too much commission, and how Siro fixes this." + + // Mock implementation + $prompt = "Write a short 20-word script about: " . $topic; + $mockScript = "Drivers are tired of high commissions. Siro is the solution with 0% commission forever. Join Siro today!"; + + return [ + 'status' => 'success', + 'script' => $mockScript + ]; + } + + // Abstracted voice generation + public function generateVoice($scriptText) { + // Here you would call ElevenLabs API (or similar) to convert text to speech + // It returns an MP3 file or URL + + // Mock implementation + $mockAudioUrl = "https://example.com/audio_mock_".time().".mp3"; + + return [ + 'status' => 'success', + 'audio_url' => $mockAudioUrl + ]; + } + + // Abstracted video rendering + public function renderVideo($scriptText, $audioUrl, $backgroundVideoUrl = null) { + // Here you would call Creatomate or HeyGen to render the final MP4 + // Combining the audio, background, and burning the captions on screen + + // Mock implementation + $mockVideoUrl = "https://example.com/rendered_video_".time().".mp4"; + + return [ + 'status' => 'success', + 'video_url' => $mockVideoUrl + ]; + } +} diff --git a/backend/marketing_engine/services/ContentWorkflow.php b/backend/marketing_engine/services/ContentWorkflow.php new file mode 100644 index 00000000..b467f568 --- /dev/null +++ b/backend/marketing_engine/services/ContentWorkflow.php @@ -0,0 +1,105 @@ +generator = new AIVideoGenerator(); + $this->con = Database::get('main'); + } + + /** + * Executes the next pending step in the content pipeline + */ + public function processPipeline() { + // 1. Check for pending topics to generate scripts + $this->processPendingScripts(); + + // 2. Check for generated scripts to create voice + $this->processPendingVoices(); + + // 3. Check for generated voices to render video + $this->processPendingVideos(); + + // 4. Check for rendered videos to queue marketing tasks + $this->queueUploadTasks(); + } + + private function processPendingScripts() { + $stmt = $this->con->prepare("SELECT * FROM content_pipeline WHERE status = 'pending' LIMIT 1"); + $stmt->execute(); + $job = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($job) { + $result = $this->generator->generateScript($job['topic']); + if ($result['status'] === 'success') { + $update = $this->con->prepare("UPDATE content_pipeline SET script_text = ?, status = 'script_generated' WHERE id = ?"); + $update->execute([$result['script'], $job['id']]); + } + } + } + + private function processPendingVoices() { + $stmt = $this->con->prepare("SELECT * FROM content_pipeline WHERE status = 'script_generated' LIMIT 1"); + $stmt->execute(); + $job = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($job) { + $result = $this->generator->generateVoice($job['script_text']); + if ($result['status'] === 'success') { + $update = $this->con->prepare("UPDATE content_pipeline SET voice_url = ?, status = 'voice_generated' WHERE id = ?"); + $update->execute([$result['audio_url'], $job['id']]); + } + } + } + + private function processPendingVideos() { + $stmt = $this->con->prepare("SELECT * FROM content_pipeline WHERE status = 'voice_generated' LIMIT 1"); + $stmt->execute(); + $job = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($job) { + $result = $this->generator->renderVideo($job['script_text'], $job['voice_url']); + if ($result['status'] === 'success') { + $update = $this->con->prepare("UPDATE content_pipeline SET video_url = ?, status = 'video_rendered' WHERE id = ?"); + $update->execute([$result['video_url'], $job['id']]); + } + } + } + + private function queueUploadTasks() { + $stmt = $this->con->prepare("SELECT * FROM content_pipeline WHERE status = 'video_rendered' LIMIT 1"); + $stmt->execute(); + $job = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($job) { + // Create upload tasks for TikTok and YouTube Shorts + $insert = $this->con->prepare(" + INSERT INTO marketing_tasks (platform, type, content_text, media_url, status) + VALUES + ('tiktok', 'upload_video', ?, ?, 'pending'), + ('youtube', 'upload_video', ?, ?, 'pending') + "); + + // Provide a short caption based on the script + $caption = substr($job['script_text'], 0, 100) . "... #Siro #RideHailing"; + + $insert->execute([ + $caption, $job['video_url'], + $caption, $job['video_url'] + ]); + + // Mark job as published (or handed off to bots) + $update = $this->con->prepare("UPDATE content_pipeline SET status = 'published' WHERE id = ?"); + $update->execute([$job['id']]); + } + } +} diff --git a/backend/bot/social_media_bot/social_worker.php b/backend/marketing_engine/social_worker.php similarity index 100% rename from backend/bot/social_media_bot/social_worker.php rename to backend/marketing_engine/social_worker.php diff --git a/backend/schema_primary.sql b/backend/schema_primary.sql index de236db3..75326474 100644 --- a/backend/schema_primary.sql +++ b/backend/schema_primary.sql @@ -2070,3 +2070,73 @@ CREATE TABLE `driver_streaks` ( UNIQUE KEY `driver_id` (`driver_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; +-- ========================================================== +-- Marketing Engine & Social Media Bot Combined Schema +-- ========================================================== + +-- 1. Original Social Media Tables (Facebook, Instagram) +CREATE TABLE IF NOT EXISTS `social_accounts` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `platform` ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube') NOT NULL, + `username` VARCHAR(100) NOT NULL, + `status` ENUM('active', 'restricted', 'banned') DEFAULT 'active', + `total_posts` INT DEFAULT 0, + `total_comments` INT DEFAULT 0, + `total_videos` 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, + `platform` ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube') NOT NULL, + `type` ENUM('join_group', 'read_posts', 'post_comment', 'share_link', 'upload_video', 'post_tweet') NOT NULL, + `target_url` VARCHAR(500) NULL, -- URL of the group, post, or media download + `prompt_context` TEXT NULL, + `generated_comment` TEXT NULL, + `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 +); + +-- 2. New Content Generation Pipeline Tables +CREATE TABLE IF NOT EXISTS `content_pipeline` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `topic` VARCHAR(255) NOT NULL, + `script_text` TEXT NULL, + `voice_url` VARCHAR(500) NULL, + `video_url` VARCHAR(500) NULL, + `status` ENUM('pending', 'script_generated', 'voice_generated', 'video_rendered', 'published', 'failed') DEFAULT 'pending', + `error_message` TEXT NULL, + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS `api_quotas` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `service_name` ENUM('gemini', 'elevenlabs', 'creatomate', 'heygen') NOT NULL, + `daily_usage` INT DEFAULT 0, + `quota_limit` INT DEFAULT 10, + `last_reset` DATE NOT NULL +); + +INSERT IGNORE INTO `api_quotas` (`service_name`, `daily_usage`, `quota_limit`, `last_reset`) VALUES +('gemini', 0, 100, CURDATE()), +('elevenlabs', 0, 5, CURDATE()), +('creatomate', 0, 2, CURDATE()); 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 index 301e8b56..0027fde1 100644 --- 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 @@ -10,28 +10,41 @@ 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 BASE_URL = "https://jordan-siro.intaleqapp.com/backend/marketing_engine/index.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 urlString = "$BASE_URL?action=get_task&platform=$platform" + android.util.Log.d("SocialBotClient", "Requesting URL: $urlString") + + val url = URL(urlString) val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.setRequestProperty("X-Bot-Token", BOT_TOKEN) val responseCode = connection.responseCode + android.util.Log.d("SocialBotClient", "Response Code: $responseCode") + if (responseCode == 200) { val scanner = Scanner(connection.inputStream) - val response = scanner.useDelimiter("\\A").next() + val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else "" scanner.close() + android.util.Log.d("SocialBotClient", "Response Body: $response") + val json = JSONObject(response) if (json.getString("status") == "success" && !json.isNull("data")) { return json.getJSONObject("data") } + } else { + val scanner = Scanner(connection.errorStream) + val errorResponse = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else "" + scanner.close() + android.util.Log.e("SocialBotClient", "Error Response Body: $errorResponse") } } catch (e: Exception) { + android.util.Log.e("SocialBotClient", "Exception in getTask: ${e.message}") e.printStackTrace() } return null 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 index 7a19f902..71166ac4 100644 --- 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 @@ -96,8 +96,22 @@ class FacebookBotService : AccessibilityService() { } "read_posts" -> { // Logic to read posts and send to backend - // ... - SocialBotClient.completeTask(task.id, "Posts read successfully") + Log.d(TAG, "Starting to scan screen for posts/comments...") + val posts = commentReader.extractPostsAndComments() + + if (posts.isNotEmpty()) { + Log.d(TAG, "===== FOUND ${posts.size} POSTS/COMMENTS =====") + for ((index, post) in posts.withIndex()) { + Log.d(TAG, "Post [${index + 1}]: $post") + } + Log.d(TAG, "===============================================") + + // TODO: Send extracted posts to backend + SocialBotClient.completeTask(task.id, "Read ${posts.size} posts successfully") + } else { + Log.d(TAG, "No valid posts found on the screen.") + SocialBotClient.failTask(task.id, "No posts found") + } } else -> { Log.w(TAG, "Unknown task type: ${task.type}") 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 index 8e2814b8..f3ad5400 100644 --- 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 @@ -5,30 +5,72 @@ import android.view.accessibility.AccessibilityNodeInfo class FacebookCommentReader(private val service: AccessibilityService) { - fun extractComments(): List { + fun extractPostsAndComments(): List { val root = service.rootInActiveWindow ?: return emptyList() - val comments = mutableListOf() + val extractedTexts = 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) + // Use a set to avoid duplicates as AccessibilityNodeInfo can be nested deeply + val uniqueTexts = mutableSetOf() + findTextNodes(root, uniqueTexts) - return comments + extractedTexts.addAll(uniqueTexts) + return extractedTexts } - private fun findTextNodes(node: AccessibilityNodeInfo?, comments: MutableList) { + private fun findTextNodes(node: AccessibilityNodeInfo?, collectedTexts: MutableSet) { 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) + val text = node.text?.toString() ?: node.contentDescription?.toString() + + if (!text.isNullOrBlank()) { + if (isValidPostText(text)) { + collectedTexts.add(text.trim()) } } for (i in 0 until node.childCount) { - findTextNodes(node.getChild(i), comments) + findTextNodes(node.getChild(i), collectedTexts) } } + + /** + * Filter out common Facebook UI elements and very short texts + * so we only send actual posts/comments to the backend. + */ + private fun isValidPostText(text: String): Boolean { + val trimmed = text.trim() + + // 1. Ignore very short texts (less than 15 characters usually aren't full posts) + if (trimmed.length < 15) return false + + // 2. Ignore common Facebook UI strings (English and Arabic) + val ignoreList = listOf( + "like", "comment", "share", "send", "write a comment", + "reply", "view more comments", "most relevant", "home", + "watch", "marketplace", "groups", "notifications", "menu", + "إعجاب", "تعليق", "مشاركة", "إرسال", "اكتب تعليقاً", + "رد", "عرض المزيد من التعليقات", "الأكثر صلة", "الصفحة الرئيسية", + "المجموعات", "الإشعارات", "القائمة", "Sponsored", "مُموَّل" + ) + + for (ignore in ignoreList) { + if (trimmed.equals(ignore, ignoreCase = true)) { + return false + } + } + + // 3. Ignore texts that look like timestamps or metrics (e.g. "2 hrs", "10K Likes") + // Simplistic check: if it's short and ends with common time units + if (trimmed.length < 25) { + val timePatterns = listOf("hrs", "mins", "ساعات", "دقيقة", "دقائق", "أمس", "yesterday") + for (pattern in timePatterns) { + if (trimmed.contains(pattern, ignoreCase = true)) { + // It's likely a timestamp, not a post + return false + } + } + } + + return true + } } 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 index acdaa2d6..ac427ee6 100644 --- 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 @@ -58,7 +58,7 @@ class InstagramNavigator(private val service: AccessibilityService) { private fun findNodeByHint(node: AccessibilityNodeInfo?, hint: String): AccessibilityNodeInfo? { if (node == null) return null - if (node.hintText?.contains(hint, ignoreCase = true) == true) return node + if (node.hintText?.toString()?.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 @@ -68,8 +68,8 @@ class InstagramNavigator(private val service: AccessibilityService) { 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 + if (node.text?.toString()?.equals(text, ignoreCase = true) == true || + node.contentDescription?.toString()?.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