Update: 2026-07-04 21:38:44

This commit is contained in:
Hamza-Ayed
2026-07-04 21:38:44 +03:00
parent df4af1ac2b
commit ce7db27936
8 changed files with 310 additions and 25 deletions
+54 -1
View File
@@ -133,7 +133,7 @@ class SiroGeminiService {
*/
public function analyzeCompetitorFormulas(
array $formulas,
string $model = 'gemini-1.5-flash'
string $model = 'gemini-flash-lite-latest'
): ?array {
if (!$this->apiKey) return null;
@@ -249,5 +249,58 @@ class SiroGeminiService {
error_log("[SiroGeminiService] Exception: " . $e->getMessage());
return null;
}
public function evaluatePostsForReporting(array $posts, string $model = 'gemini-flash-lite-latest'): ?string {
if (!$this->apiKey || empty($posts)) return null;
$postsJson = json_encode($posts, JSON_UNESCAPED_UNICODE);
$prompt = "
أنت محلل بيانات استخباراتية للسوق لتطبيق 'سيرو' لنقل الركاب.
إليك مجموعة من المنشورات والتعليقات التي جمعها الروبوت الخاص بنا من مجموعات فيسبوك اليوم:
$postsJson
المطلوب منك:
1. قراءة جميع هذه المنشورات واستخراج أي شكاوى، أسئلة، أو نقاشات تتعلق بـ (تطبيقات النقل الذكي، أسعار المحروقات، باقات الإنترنت، مشاكل السيارات).
2. تلخيص أهم هذه النقاشات في تقرير قصير ومفيد (News Report).
3. تجاهل المنشورات العشوائية أو الشخصية التي لا تفيد السوق.
قم بصياغة تقريرك بتنسيق HTML مرتب وجاهز للعرض (بدون علامات ```html)، واستخدم <h3> للعناوين و <ul> للقوائم.
إذا لم يكن هناك أي شيء مفيد، اكتب فقط: <p>لا توجد بيانات مفيدة في هذه الدفعة.</p>
";
$url = $this->baseUrl . "{$model}:generateContent?key={$this->apiKey}";
$postData = [
'contents' => [
[
'parts' => [
['text' => $prompt]
]
]
],
'generationConfig' => [
'temperature' => 0.5,
'maxOutputTokens' => 1500
]
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
return trim(preg_replace('/```html|```/', '', $text));
}
return null;
}
}
@@ -0,0 +1,9 @@
<?php
require_once __DIR__ . '/../core/bootstrap.php';
$con = Database::get('main');
// Insert the autonomous_scroll_and_reply task
$stmt = $con->prepare("INSERT INTO marketing_tasks (platform, type, status) VALUES ('facebook', 'autonomous_scroll_and_reply', 'pending')");
$stmt->execute();
echo "Task inserted successfully at " . date('Y-m-d H:i:s') . "\n";
+34 -13
View File
@@ -6,29 +6,26 @@
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
require_once __DIR__ . '/../core/Services/SiroGeminiService.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']));
}
// 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
$platform = $_GET['platform'] ?? 'facebook';
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
@@ -39,10 +36,6 @@ try {
$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']]);
@@ -78,6 +71,34 @@ try {
}
break;
case 'evaluate_posts':
$input = json_decode(file_get_contents('php://input'), true);
$posts = $input['posts'] ?? [];
if (empty($posts)) {
echo json_encode(['status' => 'success', 'data' => null]);
break;
}
$gemini = new SiroGeminiService();
$reportHtml = $gemini->evaluatePostsForReporting($posts);
if ($reportHtml) {
// Save to database
$con->query("CREATE TABLE IF NOT EXISTS `marketing_reports` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`platform` VARCHAR(50) NOT NULL,
`report_html` TEXT NOT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
)");
$stmt = $con->prepare("INSERT INTO marketing_reports (platform, report_html) VALUES (?, ?)");
$stmt->execute([$platform, $reportHtml]);
}
echo json_encode(['status' => 'success', 'message' => 'Posts evaluated and report saved']);
break;
default:
echo json_encode(['status' => 'error', 'message' => 'Invalid action']);
}
@@ -54,6 +54,43 @@ object SocialBotClient {
postData("action=complete_task", "task_id=$taskId&result=$result")
}
fun evaluatePosts(posts: List<String>): JSONObject? {
try {
val urlString = "$BASE_URL?action=evaluate_posts"
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
connection.setRequestProperty("Content-Type", "application/json")
connection.doOutput = true
val jsonBody = JSONObject()
val jsonArray = org.json.JSONArray(posts)
jsonBody.put("posts", jsonArray)
val writer = OutputStreamWriter(connection.outputStream)
writer.write(jsonBody.toString())
writer.flush()
writer.close()
val responseCode = connection.responseCode
if (responseCode == 200) {
val scanner = Scanner(connection.inputStream)
val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else ""
scanner.close()
val json = JSONObject(response)
if (json.getString("status") == "success" && !json.isNull("data")) {
return json.getJSONObject("data")
}
}
} catch (e: Exception) {
android.util.Log.e("SocialBotClient", "Exception in evaluatePosts: ${e.message}")
e.printStackTrace()
}
return null
}
fun failTask(taskId: Int, errorMessage: String) {
postData("action=fail_task", "task_id=$taskId&error_message=$errorMessage")
}
@@ -94,6 +94,29 @@ class FacebookBotService : AccessibilityService() {
}
}
}
"autonomous_scroll_and_reply" -> {
Log.d(TAG, "Starting autonomous mode. Opening Facebook app...")
navigator.openApp()
delay(3000)
var iterations = 0
while (iterations < 5) {
Log.d(TAG, "Autonomous Iteration: ${iterations + 1}")
val posts = commentReader.extractPostsAndComments()
if (posts.isNotEmpty()) {
Log.d(TAG, "Found ${posts.size} posts. Sending to Gemini for reporting...")
SocialBotClient.evaluatePosts(posts)
}
Log.d(TAG, "Scrolling down for more posts...")
navigator.scrollForward()
delay(4000) // wait for new posts to load
iterations++
}
SocialBotClient.completeTask(task.id, "Autonomous session completed")
}
"read_posts" -> {
// Logic to read posts and send to backend
Log.d(TAG, "Starting to scan screen for posts/comments...")
@@ -11,37 +11,72 @@ import kotlinx.coroutines.delay
class FacebookCommentPoster(private val service: AccessibilityService) {
suspend fun postComment(commentText: String): Boolean {
return performCommentPosting(commentText)
}
suspend fun postCommentOnSpecificPost(originalText: String, commentText: String): Boolean {
val root = service.rootInActiveWindow ?: return false
// 1. Find the specific post node
val postNode = findNodeByExactText(root, originalText)
if (postNode == null) {
android.util.Log.e("FacebookCommentPoster", "Could not find original post text on screen")
return false
}
// 2. Find the comment button associated with this post
// We go up to the parent container and look for a "Comment" or "تعليق" button
var commentBtn = findCommentButtonNearby(postNode)
if (commentBtn == null) {
// Fallback: just search the whole screen for the first comment button
commentBtn = findNodeByContentDescription(root, "Comment") ?: findNodeByContentDescription(root, "تعليق")
}
if (commentBtn != null) {
commentBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
delay(3000) // wait for comment overlay to open
val success = performCommentPosting(commentText)
// Go back to feed
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
delay(2000)
return success
}
android.util.Log.e("FacebookCommentPoster", "Could not find comment button")
return false
}
private suspend fun performCommentPosting(commentText: String): Boolean {
val root = service.rootInActiveWindow ?: return false
// 1. Find the comment input box.
// In Facebook, it might have contentDescription like "Write a comment..." or similar.
val inputNode = findNodeByContentDescription(root, "Write a comment")
?: findNodeByContentDescription(root, "اكتب تعليق")
?: findNodeByClassName(root, "android.widget.EditText")
if (inputNode == null) return false
// 2. Click the input node to focus it
inputNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
delay(1000) // wait for keyboard
delay(1000)
// 3. Set text (simulating human typing can be done by pasting chunks or using set_text)
val arguments = Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, commentText)
inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
// Wait a bit to simulate human typing delay
val randomTypingDelay = (commentText.length * 50).toLong().coerceAtMost(5000L)
val randomTypingDelay = (commentText.length * 50).toLong().coerceAtMost(4000L)
delay(randomTypingDelay)
// 4. Find and click the send button
// Often has a content description like "Send" or it's an ImageView next to EditText
val sendBtn = findNodeByContentDescription(service.rootInActiveWindow, "Send")
?: findNodeByContentDescription(service.rootInActiveWindow, "إرسال")
if (sendBtn != null) {
sendBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
delay(2000)
return true
}
return false
}
@@ -73,4 +108,33 @@ class FacebookCommentPoster(private val service: AccessibilityService) {
}
return null
}
private fun findNodeByExactText(node: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? {
if (node == null) return null
val nodeText = node.text?.toString()?.trim() ?: node.contentDescription?.toString()?.trim()
if (nodeText == text.trim()) {
return node
}
for (i in 0 until node.childCount) {
val child = findNodeByExactText(node.getChild(i), text)
if (child != null) return child
}
return null
}
private fun findCommentButtonNearby(postNode: AccessibilityNodeInfo): AccessibilityNodeInfo? {
// Go up the view hierarchy to find the container
var parent = postNode.parent
var depth = 0
while (parent != null && depth < 5) {
val btn = findNodeByContentDescription(parent, "Comment")
?: findNodeByContentDescription(parent, "تعليق")
if (btn != null) return btn
parent = parent.parent
depth++
}
return null
}
}
@@ -50,7 +50,8 @@ class FacebookCommentReader(private val service: AccessibilityService) {
"watch", "marketplace", "groups", "notifications", "menu",
"إعجاب", "تعليق", "مشاركة", "إرسال", "اكتب تعليقاً",
"رد", "عرض المزيد من التعليقات", "الأكثر صلة", "الصفحة الرئيسية",
"المجموعات", "الإشعارات", "القائمة", "Sponsored", "مُموَّل"
"المجموعات", "الإشعارات", "القائمة", "Sponsored", "مُموَّل",
"انتقال إلى الملف الشخصي", "اكتب تعليقًا...", "بحث في المجموعات"
)
for (ignore in ignoreList) {
@@ -59,6 +60,20 @@ class FacebookCommentReader(private val service: AccessibilityService) {
}
}
// 2.5 Ignore UI patterns (Partial matches)
val ignorePatterns = listOf(
"علامة التبويب", "صورة ملف", "اقتراح", "لم تتم مشاهدتها",
"خيارات إضافية", "عرض المزيد من الخيارات", "تحديد صور أو مقاطع فيديو",
"إنشاء، اضغط ضغطًا مزدوجًا", "إزالة اقتراح", "عرض كل اقتراحات",
"زر أعجبني", "الزر مشاركة", "Action chip profile picture"
)
for (pattern in ignorePatterns) {
if (trimmed.contains(pattern, 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) {
@@ -23,4 +23,67 @@ class FacebookNavigator(private val service: AccessibilityService) {
// Wait for app to open
delay(3000)
}
suspend fun openApp() {
val launchIntent = service.packageManager.getLaunchIntentForPackage("com.facebook.katana")
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
service.startActivity(launchIntent)
delay(5000) // Wait 5 seconds for Facebook to load completely
// Try to navigate to Groups Tab automatically
openGroupsTab()
} else {
android.util.Log.e("FacebookNavigator", "Facebook app is not installed.")
}
}
private suspend fun openGroupsTab() {
val root = service.rootInActiveWindow ?: return
// Search for Groups tab
val groupsTab = findNodeByContentDescription(root, "المجموعات")
?: findNodeByContentDescription(root, "Groups")
if (groupsTab != null) {
groupsTab.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
delay(3000) // wait for Groups tab to load
}
}
suspend fun scrollForward(): Boolean {
val root = service.rootInActiveWindow ?: return false
return performScroll(root)
}
private fun performScroll(node: android.view.accessibility.AccessibilityNodeInfo?): Boolean {
if (node == null) return false
if (node.isScrollable && node.className?.toString()?.contains("RecyclerView") == true) {
val scrolled = node.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
if (scrolled) return true
}
for (i in 0 until node.childCount) {
if (performScroll(node.getChild(i))) {
return true
}
}
return false
}
private fun findNodeByContentDescription(node: android.view.accessibility.AccessibilityNodeInfo?, descPrefix: String): android.view.accessibility.AccessibilityNodeInfo? {
if (node == null) return null
if (node.contentDescription?.startsWith(descPrefix, ignoreCase = true) == true ||
node.text?.startsWith(descPrefix, ignoreCase = true) == true) {
return node
}
for (i in 0 until node.childCount) {
val child = findNodeByContentDescription(node.getChild(i), descPrefix)
if (child != null) return child
}
return null
}
}