Update: 2026-07-04 20:53:30
This commit is contained in:
@@ -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');
|
||||
@@ -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";
|
||||
@@ -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']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+16
-2
@@ -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}")
|
||||
|
||||
+55
-13
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user