Update: 2026-07-04 20:53:30

This commit is contained in:
Hamza-Ayed
2026-07-04 20:53:30 +03:00
parent 3c06b3624b
commit df4af1ac2b
15 changed files with 493 additions and 64 deletions
-43
View File
@@ -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');
+88
View File
@@ -0,0 +1,88 @@
<?php
// ============================================================
// marketing_engine/index.php
// Main API endpoint for the Marketing Microservice
// ============================================================
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
header('Content-Type: application/json');
// Simple bot authentication
$headers = getallheaders();
$botToken = $headers['X-Bot-Token'] ?? '';
if ($botToken !== 'YOUR_SECRET_BOT_TOKEN') {
// http_response_code(401);
// exit(json_encode(['status' => '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()]);
}
@@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../core/bootstrap.php';
$con = Database::get('main');
// Insert into marketing_tasks
$con->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";
+70
View File
@@ -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());
@@ -0,0 +1,59 @@
<?php
// ============================================================
// marketing_engine/services/AIVideoGenerator.php
// Wrapper and Abstract Interfaces for AI Generation APIs
// ============================================================
class AIVideoGenerator {
private $elevenLabsApiKey;
private $creatomateApiKey;
private $geminiApiKey;
public function __construct() {
$this->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
];
}
}
@@ -0,0 +1,105 @@
<?php
// ============================================================
// marketing_engine/services/ContentWorkflow.php
// Orchestrates the entire content generation pipeline
// ============================================================
require_once __DIR__ . '/AIVideoGenerator.php';
require_once __DIR__ . '/../../core/bootstrap.php';
class ContentWorkflow {
private $generator;
private $con;
public function __construct() {
$this->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']]);
}
}
}
+70
View File
@@ -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());
@@ -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
@@ -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}")
@@ -5,30 +5,72 @@ import android.view.accessibility.AccessibilityNodeInfo
class FacebookCommentReader(private val service: AccessibilityService) {
fun extractComments(): List<String> {
fun extractPostsAndComments(): List<String> {
val root = service.rootInActiveWindow ?: return emptyList()
val comments = mutableListOf<String>()
val extractedTexts = mutableListOf<String>()
// 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<String>()
findTextNodes(root, uniqueTexts)
return comments
extractedTexts.addAll(uniqueTexts)
return extractedTexts
}
private fun findTextNodes(node: AccessibilityNodeInfo?, comments: MutableList<String>) {
private fun findTextNodes(node: AccessibilityNodeInfo?, collectedTexts: MutableSet<String>) {
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
}
}
@@ -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