Update: 2026-07-04 21:38:44
This commit is contained in:
@@ -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";
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
+23
@@ -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...")
|
||||
|
||||
+74
-10
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -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) {
|
||||
|
||||
+63
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user