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`)
|
UNIQUE KEY `driver_id` (`driver_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) 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
|
* Client to connect to the social_worker.php endpoint
|
||||||
*/
|
*/
|
||||||
object SocialBotClient {
|
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"
|
private const val BOT_TOKEN = "YOUR_SECRET_BOT_TOKEN"
|
||||||
|
|
||||||
fun getTask(platform: String): JSONObject? {
|
fun getTask(platform: String): JSONObject? {
|
||||||
try {
|
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
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
connection.requestMethod = "GET"
|
connection.requestMethod = "GET"
|
||||||
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
|
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
|
||||||
|
|
||||||
val responseCode = connection.responseCode
|
val responseCode = connection.responseCode
|
||||||
|
android.util.Log.d("SocialBotClient", "Response Code: $responseCode")
|
||||||
|
|
||||||
if (responseCode == 200) {
|
if (responseCode == 200) {
|
||||||
val scanner = Scanner(connection.inputStream)
|
val scanner = Scanner(connection.inputStream)
|
||||||
val response = scanner.useDelimiter("\\A").next()
|
val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else ""
|
||||||
scanner.close()
|
scanner.close()
|
||||||
|
|
||||||
|
android.util.Log.d("SocialBotClient", "Response Body: $response")
|
||||||
|
|
||||||
val json = JSONObject(response)
|
val json = JSONObject(response)
|
||||||
if (json.getString("status") == "success" && !json.isNull("data")) {
|
if (json.getString("status") == "success" && !json.isNull("data")) {
|
||||||
return json.getJSONObject("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) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("SocialBotClient", "Exception in getTask: ${e.message}")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
+16
-2
@@ -96,8 +96,22 @@ class FacebookBotService : AccessibilityService() {
|
|||||||
}
|
}
|
||||||
"read_posts" -> {
|
"read_posts" -> {
|
||||||
// Logic to read posts and send to backend
|
// Logic to read posts and send to backend
|
||||||
// ...
|
Log.d(TAG, "Starting to scan screen for posts/comments...")
|
||||||
SocialBotClient.completeTask(task.id, "Posts read successfully")
|
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 -> {
|
else -> {
|
||||||
Log.w(TAG, "Unknown task type: ${task.type}")
|
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) {
|
class FacebookCommentReader(private val service: AccessibilityService) {
|
||||||
|
|
||||||
fun extractComments(): List<String> {
|
fun extractPostsAndComments(): List<String> {
|
||||||
val root = service.rootInActiveWindow ?: return emptyList()
|
val root = service.rootInActiveWindow ?: return emptyList()
|
||||||
val comments = mutableListOf<String>()
|
val extractedTexts = mutableListOf<String>()
|
||||||
|
|
||||||
// Facebook UI is complex and changes often.
|
// Use a set to avoid duplicates as AccessibilityNodeInfo can be nested deeply
|
||||||
// This is a simplified logic. We'd usually look for nodes with text content inside a RecyclerView
|
val uniqueTexts = mutableSetOf<String>()
|
||||||
findTextNodes(root, comments)
|
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 == null) return
|
||||||
|
|
||||||
if (node.text != null && node.text.isNotEmpty()) {
|
val text = node.text?.toString() ?: node.contentDescription?.toString()
|
||||||
// Add filtering logic to ensure it's a comment and not just UI text
|
|
||||||
val text = node.text.toString()
|
if (!text.isNullOrBlank()) {
|
||||||
if (text.length > 10) { // arbitrary filter
|
if (isValidPostText(text)) {
|
||||||
comments.add(text)
|
collectedTexts.add(text.trim())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i in 0 until node.childCount) {
|
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? {
|
private fun findNodeByHint(node: AccessibilityNodeInfo?, hint: String): AccessibilityNodeInfo? {
|
||||||
if (node == null) return null
|
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) {
|
for (i in 0 until node.childCount) {
|
||||||
val found = findNodeByHint(node.getChild(i), hint)
|
val found = findNodeByHint(node.getChild(i), hint)
|
||||||
if (found != null) return found
|
if (found != null) return found
|
||||||
@@ -68,8 +68,8 @@ class InstagramNavigator(private val service: AccessibilityService) {
|
|||||||
|
|
||||||
private fun findNodeByText(node: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? {
|
private fun findNodeByText(node: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? {
|
||||||
if (node == null) return null
|
if (node == null) return null
|
||||||
if (node.text?.equals(text, ignoreCase = true) == true ||
|
if (node.text?.toString()?.equals(text, ignoreCase = true) == true ||
|
||||||
node.contentDescription?.equals(text, ignoreCase = true) == true) return node
|
node.contentDescription?.toString()?.equals(text, ignoreCase = true) == true) return node
|
||||||
for (i in 0 until node.childCount) {
|
for (i in 0 until node.childCount) {
|
||||||
val found = findNodeByText(node.getChild(i), text)
|
val found = findNodeByText(node.getChild(i), text)
|
||||||
if (found != null) return found
|
if (found != null) return found
|
||||||
|
|||||||
Reference in New Issue
Block a user