Update: 2026-07-04 18:43:56
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// account_manager.php
|
||||
// Handles rotation and selection of social media accounts
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
|
||||
class AccountManager {
|
||||
private $con;
|
||||
|
||||
public function __construct() {
|
||||
$this->con = Database::get('main');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an available account for a specific platform that hasn't been used too recently.
|
||||
*
|
||||
* @param string $platform 'facebook' or 'instagram'
|
||||
* @param int $cooldownMinutes Minimum minutes since last active
|
||||
* @return array|null Account details or null if none available
|
||||
*/
|
||||
public function getAvailableAccount($platform, $cooldownMinutes = 30) {
|
||||
// Select an active account that was last active more than X minutes ago,
|
||||
// or has never been active (last_active is NULL).
|
||||
// Order by last_active ascending to rotate through them (least recently used first).
|
||||
|
||||
$stmt = $this->con->prepare("
|
||||
SELECT id, username, total_posts, total_comments
|
||||
FROM social_accounts
|
||||
WHERE platform = ?
|
||||
AND status = 'active'
|
||||
AND (last_active IS NULL OR last_active <= DATE_SUB(NOW(), INTERVAL ? MINUTE))
|
||||
ORDER BY last_active ASC, id ASC
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmt->execute([$platform, $cooldownMinutes]);
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the last active timestamp for an account
|
||||
*/
|
||||
public function markAccountActive($accountId) {
|
||||
$stmt = $this->con->prepare("UPDATE social_accounts SET last_active = NOW() WHERE id = ?");
|
||||
$stmt->execute([$accountId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment comment count
|
||||
*/
|
||||
public function incrementCommentCount($accountId) {
|
||||
$stmt = $this->con->prepare("UPDATE social_accounts SET total_comments = total_comments + 1, last_active = NOW() WHERE id = ?");
|
||||
$stmt->execute([$accountId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an account as restricted or banned
|
||||
*/
|
||||
public function markAccountStatus($accountId, $status) {
|
||||
if (!in_array($status, ['active', 'restricted', 'banned'])) return false;
|
||||
|
||||
$stmt = $this->con->prepare("UPDATE social_accounts SET status = ? WHERE id = ?");
|
||||
return $stmt->execute([$status, $accountId]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// gemini_comment_generator.php
|
||||
// Connects to Gemini API to generate natural, driver-like comments
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../../functions.php';
|
||||
|
||||
// The API key should be stored in .env or the database. For now, reading from env.
|
||||
$geminiApiKey = getenv('GEMINI_API_KEY');
|
||||
|
||||
if (!$geminiApiKey) {
|
||||
die("Error: GEMINI_API_KEY not set in environment.\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a comment using Gemini API
|
||||
*
|
||||
* @param string $postContext The content of the post or the discussion
|
||||
* @param string $intent What the bot should try to achieve (e.g. "promote_siro", "agree_and_empathize")
|
||||
* @return string The generated comment
|
||||
*/
|
||||
function generateCommentWithGemini($postContext, $intent) {
|
||||
global $geminiApiKey;
|
||||
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=" . $geminiApiKey;
|
||||
|
||||
$systemInstruction = "أنت سائق سيارة أجرة في الأردن أو السعودية. تتحدث بلهجة عامية بسيطة جداً، وتكتب أحياناً بأسلوب عفوي (ممكن مع أخطاء مطبعية بسيطة لتكون طبيعياً). أنت تستخدم تطبيق 'سيرو' (Siro) للنقل الذكي وتراه التطبيق الأفضل لأنه يوفر دخل ممتاز ولا يظلم السائق. لا تتحدث كمسوق لشركة، بل كزميل ناصح.";
|
||||
|
||||
$prompt = "السياق/المنشور الذي تقرأه الآن: \n\"" . $postContext . "\"\n\n";
|
||||
|
||||
if ($intent === 'promote_siro') {
|
||||
$prompt .= "المطلوب: اكتب تعليقاً تتعاطف فيه مع المشكلة المذكورة (إن وجدت)، وانصح الزملاء بتجربة تطبيق 'سيرو' كحل واقعي وممتاز لكباتن النقل الذكي. اجعل التعليق قصيراً ومباشراً.";
|
||||
} elseif ($intent === 'answer_question') {
|
||||
$prompt .= "المطلوب: أجب على السؤال المطروح في المنشور بوضوح بناءً على خبرتك مع تطبيق 'سيرو'.";
|
||||
} else {
|
||||
$prompt .= "المطلوب: اكتب تعليقاً متفاعلاً بشكل عام مع المنشور بأسلوب السائقين.";
|
||||
}
|
||||
|
||||
$data = [
|
||||
"contents" => [
|
||||
[
|
||||
"parts" => [
|
||||
["text" => $systemInstruction . "\n\n" . $prompt]
|
||||
]
|
||||
]
|
||||
],
|
||||
"generationConfig" => [
|
||||
"temperature" => 0.7, // A bit of creativity for varied responses
|
||||
"maxOutputTokens" => 150
|
||||
]
|
||||
];
|
||||
|
||||
$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($data));
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200) {
|
||||
$result = json_decode($response, true);
|
||||
if (isset($result['candidates'][0]['content']['parts'][0]['text'])) {
|
||||
return trim($result['candidates'][0]['content']['parts'][0]['text']);
|
||||
}
|
||||
}
|
||||
|
||||
return "والله يا صاحبي جربت تطبيق سيرو وارتحت كثير من المشاكل هذي، جربه ما بتندم."; // Fallback response
|
||||
}
|
||||
|
||||
// Example usage if called directly via CLI:
|
||||
if (php_sapi_name() === 'cli') {
|
||||
echo "Testing Gemini Generation...\n";
|
||||
$testContext = "التطبيقات الثانية بتاخذ عمولة عالية جداً ومش ملحقين بنزين!";
|
||||
$generated = generateCommentWithGemini($testContext, 'promote_siro');
|
||||
echo "Generated Comment: \n" . $generated . "\n";
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// schedule_manager.php
|
||||
// Creates and schedules tasks for the bots
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
require_once __DIR__ . '/gemini_comment_generator.php';
|
||||
|
||||
class ScheduleManager {
|
||||
private $con;
|
||||
|
||||
public function __construct() {
|
||||
$this->con = Database::get('main');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new task and optionally generate a comment for it using Gemini.
|
||||
*
|
||||
* @param string $platform 'facebook' or 'instagram'
|
||||
* @param string $type 'join_group', 'read_posts', 'post_comment', 'share_link'
|
||||
* @param string|null $targetUrl The group or post URL
|
||||
* @param string|null $promptContext Context for Gemini
|
||||
* @param int|null $accountId Specific account ID or null for any available
|
||||
* @param int $delayMinutes How many minutes to delay this task
|
||||
*/
|
||||
public function scheduleTask($platform, $type, $targetUrl = null, $promptContext = null, $accountId = null, $delayMinutes = 0) {
|
||||
// Quiet hours check (e.g. don't schedule tasks between 1 AM and 6 AM)
|
||||
$currentHour = (int)date('H');
|
||||
if ($currentHour >= 1 && $currentHour < 6) {
|
||||
// Push it to 6 AM if we are in quiet hours
|
||||
$hoursToAdd = 6 - $currentHour;
|
||||
$delayMinutes += ($hoursToAdd * 60);
|
||||
}
|
||||
|
||||
$scheduledAt = date('Y-m-d H:i:s', strtotime("+$delayMinutes minutes"));
|
||||
|
||||
$generatedComment = null;
|
||||
if (($type === 'post_comment' || $type === 'share_link') && $promptContext) {
|
||||
// Pre-generate the comment using Gemini
|
||||
// If it's share_link, we want an intent to promote, otherwise just answer/interact
|
||||
$intent = ($type === 'share_link') ? 'promote_siro' : 'answer_question';
|
||||
$generatedComment = generateCommentWithGemini($promptContext, $intent);
|
||||
}
|
||||
|
||||
$stmt = $this->con->prepare("
|
||||
INSERT INTO social_tasks (account_id, platform, type, target_url, prompt_context, generated_comment, scheduled_at, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
|
||||
");
|
||||
|
||||
return $stmt->execute([
|
||||
$accountId,
|
||||
$platform,
|
||||
$type,
|
||||
$targetUrl,
|
||||
$promptContext,
|
||||
$generatedComment,
|
||||
$scheduledAt
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage via CLI
|
||||
if (php_sapi_name() === 'cli' && isset($argv[1]) && $argv[1] === 'test_schedule') {
|
||||
$sm = new ScheduleManager();
|
||||
$context = "شو رأيكم بتطبيق سيرو يا كباتن؟ حد جربه؟";
|
||||
$success = $sm->scheduleTask('facebook', 'post_comment', 'https://facebook.com/groups/amman.drivers/post/12345', $context, null, 5);
|
||||
echo $success ? "Task scheduled successfully!\n" : "Failed to schedule task.\n";
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
-- 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,108 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// social_worker.php
|
||||
// Endpoint for the Android Social Media Bot
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../core/bootstrap.php';
|
||||
require_once __DIR__ . '/../../functions.php';
|
||||
|
||||
// Authentication and validation could be added here similar to driver_socket.php
|
||||
// For now, let's keep it simple or use a static token for the bot
|
||||
$headers = getallheaders();
|
||||
$botToken = $headers['X-Bot-Token'] ?? '';
|
||||
if ($botToken !== 'YOUR_SECRET_BOT_TOKEN') {
|
||||
// In production, use a secure token or JWT
|
||||
// http_response_code(401);
|
||||
// exit(json_encode(['status' => 'error', 'message' => 'Unauthorized']));
|
||||
}
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
|
||||
switch ($action) {
|
||||
case 'get_task':
|
||||
// The bot asks for a task to do
|
||||
$platform = $_GET['platform'] ?? 'facebook';
|
||||
|
||||
// Find a pending task that is scheduled for now or earlier
|
||||
$stmt = $con->prepare("
|
||||
SELECT id, type, target_url, prompt_context, generated_comment
|
||||
FROM social_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) {
|
||||
// Mark as in progress
|
||||
$update = $con->prepare("UPDATE social_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':
|
||||
// The bot reports task completion
|
||||
$taskId = $_POST['task_id'] ?? null;
|
||||
$result = $_POST['result'] ?? ''; // e.g. success or error details
|
||||
|
||||
if ($taskId) {
|
||||
$stmt = $con->prepare("UPDATE social_tasks SET status = 'completed', completed_at = NOW() WHERE id = ?");
|
||||
$stmt->execute([$taskId]);
|
||||
|
||||
// Log it
|
||||
$log = $con->prepare("INSERT INTO social_logs (task_id, log_level, message) VALUES (?, 'info', ?)");
|
||||
$log->execute([$taskId, "Task completed: " . substr($result, 0, 200)]);
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'task_id required']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fail_task':
|
||||
// The bot reports task failure
|
||||
$taskId = $_POST['task_id'] ?? null;
|
||||
$errorMsg = $_POST['error_message'] ?? 'Unknown error';
|
||||
|
||||
if ($taskId) {
|
||||
$stmt = $con->prepare("UPDATE social_tasks SET status = 'failed', error_message = ? WHERE id = ?");
|
||||
$stmt->execute([$errorMsg, $taskId]);
|
||||
|
||||
// Log it
|
||||
$log = $con->prepare("INSERT INTO social_logs (task_id, log_level, message) VALUES (?, 'error', ?)");
|
||||
$log->execute([$taskId, "Task failed: " . substr($errorMsg, 0, 200)]);
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
} else {
|
||||
echo json_encode(['status' => 'error', 'message' => 'task_id required']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'log':
|
||||
// The bot sends a general log
|
||||
$accountId = $_POST['account_id'] ?? null;
|
||||
$level = $_POST['level'] ?? 'info';
|
||||
$message = $_POST['message'] ?? '';
|
||||
|
||||
$log = $con->prepare("INSERT INTO social_logs (account_id, log_level, message) VALUES (?, ?, ?)");
|
||||
$log->execute([$accountId, $level, $message]);
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['status' => 'error', 'message' => 'Invalid action']);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -29,10 +29,10 @@ try {
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. طلب الـ IDs والمواقع من سيرفر اللوكيشن (Redis API)
|
||||
// 2. طلب بيانات السائقين من سيرفر اللوكيشن (Redis API)
|
||||
// — يرجع cache hits لو موجودة، وإلا يرجع IDs فقط
|
||||
// ==========================================
|
||||
$locationServerUrl = getenv('LOCATION_API_URL');
|
||||
// تأكد من المسار الصحيح للمفتاح على السيرفر الرئيسي
|
||||
$INTERNAL_KEY = trim(file_get_contents(getenv('INTERNAL_SOCKET_KEY_PATH')));
|
||||
|
||||
$ch = curl_init();
|
||||
@@ -41,7 +41,7 @@ try {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
|
||||
'lat' => $lat,
|
||||
'lng' => $lng,
|
||||
'radius' => 5, // 5 كم كافية للعرض
|
||||
'radius' => 5,
|
||||
'limit' => 50
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
@@ -65,119 +65,115 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// تجهيز خريطة لدمج البيانات لاحقاً (ID => RedisData)
|
||||
$driversMap = [];
|
||||
$driverIds = [];
|
||||
// تقسيم: cache hits → نستخدمها فوراً، cache misses → نحتاج MySQL
|
||||
$final_result = [];
|
||||
$mysqlIds = [];
|
||||
$driversMap = [];
|
||||
$serverNow = date('Y-m-d H:i:s');
|
||||
|
||||
foreach ($redisDrivers as $d) {
|
||||
$driverIds[] = $d['id'];
|
||||
$driversMap[$d['id']] = $d;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 3. جلب التفاصيل الكاملة من MySQL (مثل الملف القديم تماماً)
|
||||
// ==========================================
|
||||
|
||||
// تجهيز الـ Placeholders
|
||||
$placeholders = implode(',', array_fill(0, count($driverIds), '?'));
|
||||
|
||||
// الاستعلام الشامل (نفس الحقول القديمة)
|
||||
$sql_drivers_info = "
|
||||
SELECT
|
||||
d.id AS driver_id,
|
||||
d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus,
|
||||
cr.make,
|
||||
cr.car_plate,
|
||||
cr.model,
|
||||
cr.color, cr.vin, cr.color_hex,
|
||||
cr.year,
|
||||
cr.vehicle_category_id,
|
||||
dt.token,
|
||||
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver
|
||||
FROM driver d
|
||||
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
|
||||
LEFT JOIN driverToken dt ON dt.captain_id = d.id
|
||||
LEFT JOIN (
|
||||
SELECT driver_id, AVG(rating) AS ratingDriver
|
||||
FROM ratingDriver
|
||||
GROUP BY driver_id
|
||||
) rdAvg ON rdAvg.driver_id = d.id
|
||||
WHERE d.id IN ($placeholders)
|
||||
AND COALESCE(cr.year, 0) > 2000
|
||||
-- AND (cr.make NOT LIKE '%دراج%' AND cr.model NOT LIKE '%دراج%')
|
||||
";
|
||||
|
||||
$stmt = $con->prepare($sql_drivers_info);
|
||||
$stmt->execute($driverIds);
|
||||
$drivers_db = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($drivers_db)) {
|
||||
jsonSuccess([], "No matching drivers in DB");
|
||||
exit;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 4. معالجة البيانات، الدمج، وفك التشفير
|
||||
// ==========================================
|
||||
|
||||
$final_result = [];
|
||||
$serverNow = date('Y-m-d H:i:s');
|
||||
$fieldsToDecrypt = ['phone','email','gender','birthdate','first_name','last_name','token','car_plate','vin'];
|
||||
|
||||
// الهاش الخاص بالإناث (لتحديد النوع لاحقاً إذا لزم الأمر)
|
||||
// $femaleHash = 'bQ6yWJ2EVXKZooHdGclvmFiDlZCM8UYeO+ILFjDUvpQ=';
|
||||
|
||||
foreach ($drivers_db as $row) {
|
||||
$did = $row['driver_id'];
|
||||
|
||||
// دمج بيانات الموقع الحية من الريدز (أهم خطوة)
|
||||
if (isset($driversMap[$did])) {
|
||||
$redisInfo = $driversMap[$did];
|
||||
$row['latitude'] = $redisInfo['lat'];
|
||||
$row['longitude'] = $redisInfo['lng'];
|
||||
$row['heading'] = $redisInfo['heading'];
|
||||
$row['speed'] = $redisInfo['speed'];
|
||||
// $row['distance'] = $redisInfo['distance']; // إذا أردت إضافتها
|
||||
if (!empty($d['cached']) && !empty($d['first_name'])) {
|
||||
// ✅ Cache hit — بيانات السائق كاملة من Redis, نستخدمها مباشرة
|
||||
$d['serverNow'] = $serverNow;
|
||||
$final_result[] = $d;
|
||||
} else {
|
||||
// حالة نادرة: السائق موجود في الاستعلام ولكن ليس في مصفوفة الريدز (لا يجب أن تحدث)
|
||||
continue;
|
||||
// ❌ Cache miss — نحتاج نجيب البيانات من MySQL
|
||||
$mysqlIds[] = $d['id'];
|
||||
$driversMap[$d['id']] = $d;
|
||||
}
|
||||
}
|
||||
|
||||
$row['serverNow'] = $serverNow;
|
||||
|
||||
// فك التشفير (Decrypt)
|
||||
foreach ($fieldsToDecrypt as $field) {
|
||||
if (isset($row[$field]) && $row[$field] !== null && $row[$field] !== '') {
|
||||
try {
|
||||
$row[$field] = $encryptionHelper->decryptData($row[$field]);
|
||||
} catch (Exception $e) {
|
||||
$row[$field] = null;
|
||||
// ==========================================
|
||||
// 3. فقط للسائقين اللي ما فيهم Cache: MySQL
|
||||
// ==========================================
|
||||
if (!empty($mysqlIds)) {
|
||||
$placeholders = implode(',', array_fill(0, count($mysqlIds), '?'));
|
||||
|
||||
$sql_drivers_info = "
|
||||
SELECT
|
||||
d.id AS driver_id,
|
||||
d.phone, d.email, d.birthdate, d.first_name, d.last_name, d.gender, d.maritalStatus,
|
||||
cr.make, cr.car_plate, cr.model, cr.color, cr.vin, cr.color_hex, cr.year, cr.vehicle_category_id,
|
||||
dt.token,
|
||||
COALESCE(rdAvg.ratingDriver, 0) AS ratingDriver
|
||||
FROM driver d
|
||||
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
|
||||
LEFT JOIN driverToken dt ON dt.captain_id = d.id
|
||||
LEFT JOIN (
|
||||
SELECT driver_id, AVG(rating) AS ratingDriver FROM ratingDriver GROUP BY driver_id
|
||||
) rdAvg ON rdAvg.driver_id = d.id
|
||||
WHERE d.id IN ($placeholders)
|
||||
AND COALESCE(cr.year, 0) > 2000
|
||||
";
|
||||
|
||||
$stmt = $con->prepare($sql_drivers_info);
|
||||
$stmt->execute($mysqlIds);
|
||||
$drivers_db = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$fieldsToDecrypt = ['phone','email','gender','birthdate','first_name','last_name','token','car_plate','vin'];
|
||||
|
||||
foreach ($drivers_db as $row) {
|
||||
$did = $row['driver_id'];
|
||||
|
||||
if (isset($driversMap[$did])) {
|
||||
$redisInfo = $driversMap[$did];
|
||||
$row['latitude'] = $redisInfo['latitude'] ?? $redisInfo['lat'] ?? '';
|
||||
$row['longitude'] = $redisInfo['longitude'] ?? $redisInfo['lng'] ?? '';
|
||||
$row['heading'] = $redisInfo['heading'] ?? '0';
|
||||
$row['speed'] = $redisInfo['speed'] ?? '0';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row['serverNow'] = $serverNow;
|
||||
|
||||
foreach ($fieldsToDecrypt as $field) {
|
||||
if (isset($row[$field]) && $row[$field] !== null && $row[$field] !== '') {
|
||||
try {
|
||||
$row[$field] = $encryptionHelper->decryptData($row[$field]);
|
||||
} catch (Exception $e) {
|
||||
$row[$field] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($row['birthdate'])) {
|
||||
try {
|
||||
$birthdate = new DateTime($row['birthdate']);
|
||||
$today = new DateTime();
|
||||
$row['age'] = $today->diff($birthdate)->y;
|
||||
} catch (Exception $e) { $row['age'] = null; }
|
||||
} else {
|
||||
$row['age'] = null;
|
||||
}
|
||||
|
||||
$final_result[] = $row;
|
||||
|
||||
// 🆕 Fill driver:public cache for next time (async, fire-and-forget)
|
||||
sendToLocationServer('cache_driver_public', [
|
||||
'driver_id' => $did,
|
||||
'data' => json_encode([
|
||||
'first_name' => $row['first_name'] ?? '',
|
||||
'last_name' => $row['last_name'] ?? '',
|
||||
'gender' => $row['gender'] ?? '',
|
||||
'make' => $row['make'] ?? '',
|
||||
'model' => $row['model'] ?? '',
|
||||
'color' => $row['color'] ?? '',
|
||||
'color_hex' => $row['color_hex'] ?? '',
|
||||
'year' => $row['year'] ?? '',
|
||||
'car_plate' => $row['car_plate'] ?? '',
|
||||
'ratingDriver'=> $row['ratingDriver'] ?? '0',
|
||||
'latitude' => $row['latitude'] ?? '',
|
||||
'longitude' => $row['longitude'] ?? '',
|
||||
'heading' => $row['heading'] ?? '0',
|
||||
'speed' => $row['speed'] ?? '0',
|
||||
'updated_at' => time()
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
// حساب العمر
|
||||
if (!empty($row['birthdate'])) {
|
||||
try {
|
||||
$birthdate = new DateTime($row['birthdate']);
|
||||
$today = new DateTime();
|
||||
$row['age'] = $today->diff($birthdate)->y;
|
||||
} catch (Exception $e) { $row['age'] = null; }
|
||||
} else {
|
||||
$row['age'] = null;
|
||||
}
|
||||
|
||||
// إضافة نوع السيارة البسيط (اختياري، إذا كان التطبيق يعتمد عليه)
|
||||
/*
|
||||
$type = 'car';
|
||||
if ($row['vehicle_category_id'] == 2) $type = 'bike';
|
||||
elseif ($row['gender'] == 'female') $type = 'lady'; // بعد فك التشفير تكون female
|
||||
$row['type'] = $type;
|
||||
*/
|
||||
|
||||
$final_result[] = $row;
|
||||
}
|
||||
|
||||
// إرجاع النتيجة بنفس الهيكل القديم
|
||||
// إرجاع النتيجة
|
||||
jsonSuccess($final_result);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
|
||||
@@ -199,13 +199,21 @@ try {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP F — تنظيف السوق (أبلغ location server إن الرحلة محجوزة)
|
||||
// STEP F — تنظيف السوق + Cache ride state (أبلغ location server)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
sendToLocationServer('ride_taken_event', [
|
||||
'ride_id' => $rideId,
|
||||
'taken_by_driver_id' => $driverId,
|
||||
]);
|
||||
|
||||
// 🆕 Cache ride state in Redis
|
||||
sendToLocationServer('update_ride_state', [
|
||||
'ride_id' => $rideId,
|
||||
'status' => $status,
|
||||
'driver_id' => $driverId,
|
||||
'passenger_id' => $passengerIdValue ?? '',
|
||||
]);
|
||||
|
||||
error_log("[accept_ride] SUCCESS. RideID=$rideId accepted by DriverID=$driverId");
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
@@ -67,6 +67,14 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 Cache ride state in Redis
|
||||
sendToLocationServer('update_ride_state', [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'arrived',
|
||||
'driver_id' => $driverId,
|
||||
'passenger_id' => $passenger_id ?? '',
|
||||
]);
|
||||
|
||||
jsonSuccess(null, "Arrival notified successfully");
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -212,6 +212,14 @@ try {
|
||||
|
||||
$con->commit();
|
||||
|
||||
// 🆕 Cache ride state in Redis
|
||||
sendToLocationServer('update_ride_state', [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'cancelled_by_driver',
|
||||
'driver_id' => $driverId,
|
||||
'passenger_id' => $passenger_id ?? '',
|
||||
]);
|
||||
|
||||
// 5. الرد للفلاتر
|
||||
echo json_encode([
|
||||
"status" => "success",
|
||||
|
||||
@@ -146,6 +146,14 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 Cache ride state in Redis
|
||||
sendToLocationServer('update_ride_state', [
|
||||
'ride_id' => $rideId,
|
||||
'status' => 'cancelled_by_passenger',
|
||||
'driver_id' => $driverId ?? '',
|
||||
'passenger_id' => $ride['passenger_id'] ?? '',
|
||||
]);
|
||||
|
||||
jsonSuccess(null, "Ride cancelled successfully");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
|
||||
@@ -117,6 +117,15 @@ try {
|
||||
}
|
||||
|
||||
$con->commit();
|
||||
|
||||
// 🆕 Cache ride state in Redis
|
||||
sendToLocationServer('update_ride_state', [
|
||||
'ride_id' => $ride_id,
|
||||
'status' => 'started',
|
||||
'driver_id' => $driver_id,
|
||||
'passenger_id' => $passenger_id ?? '',
|
||||
]);
|
||||
|
||||
jsonSuccess(null, "Ride started successfully");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
|
||||
@@ -61,13 +61,24 @@ try {
|
||||
continue;
|
||||
}
|
||||
|
||||
$validDrivers[] = [
|
||||
'id' => $d_id,
|
||||
'distance' => $d_dist,
|
||||
'heading' => $profile['heading'] ?? 0,
|
||||
'speed' => $profile['speed'] ?? 0,
|
||||
'lat' => $d_coord[1], // Latitude من الريدز (أدق)
|
||||
'lng' => $d_coord[0] // Longitude من الريدز
|
||||
// 🆕 فحص driver:public cache (بيانات السائق الثابتة)
|
||||
$public = $redis->hgetall("driver:public:$d_id");
|
||||
$hasCache = !empty($public) && !empty($public['first_name']);
|
||||
|
||||
$validDrivers[] = $hasCache ? array_merge($public, [
|
||||
'id' => $d_id,
|
||||
'driver_id' => $d_id,
|
||||
'distance' => $d_dist,
|
||||
'cached' => true
|
||||
]) : [
|
||||
'id' => $d_id,
|
||||
'driver_id' => $d_id,
|
||||
'distance' => $d_dist,
|
||||
'heading' => $profile['heading'] ?? 0,
|
||||
'speed' => $profile['speed'] ?? 0,
|
||||
'latitude' => $d_coord[1],
|
||||
'longitude' => $d_coord[0],
|
||||
'cached' => false
|
||||
];
|
||||
|
||||
if (count($validDrivers) >= $limit) break;
|
||||
|
||||
@@ -230,6 +230,10 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
|
||||
|
||||
if (isset($ops['hmset'])) {
|
||||
$pipe->hmset($profileKey, $ops['hmset']);
|
||||
// 🆕 Cache public driver data للقراءة السريعة من get.php (24h TTL)
|
||||
$publicKey = "driver:public:$driverId";
|
||||
$pipe->hmset($publicKey, $ops['hmset']);
|
||||
$pipe->expire($publicKey, 86400);
|
||||
}
|
||||
if (isset($ops['expire'])) {
|
||||
$pipe->expire($profileKey, $ops['expire']);
|
||||
@@ -418,6 +422,34 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
|
||||
logMsg("✅ Ride #$rideId taken by #$winnerDriverId.");
|
||||
$connection->send('OK');
|
||||
|
||||
// ── 7. Update Ride State (Redis Cache فقط — بدون Forward)
|
||||
} elseif ($action === 'update_ride_state') {
|
||||
$rideId = $post['ride_id'] ?? null;
|
||||
$status = $post['status'] ?? '';
|
||||
$driverId = $post['driver_id'] ?? '';
|
||||
$passengerId = $post['passenger_id'] ?? '';
|
||||
|
||||
if (!$rideId || !$status) {
|
||||
$connection->send('Error: Missing ride_id or status');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($redis) {
|
||||
$stateKey = "ride:$rideId:state";
|
||||
$stateData = [
|
||||
'status' => $status,
|
||||
'driver_id' => $driverId,
|
||||
'passenger_id' => $passengerId,
|
||||
'updated_at' => time(),
|
||||
];
|
||||
$redis->hmset($stateKey, $stateData);
|
||||
$redis->expire($stateKey, 86400);
|
||||
|
||||
logMsg("🚗 Ride #$rideId → status: $status (cached in Redis)");
|
||||
}
|
||||
|
||||
$connection->send('OK');
|
||||
|
||||
// ── 5. Force Disconnect ───────────────────────────────
|
||||
} elseif ($action === 'force_disconnect') {
|
||||
$driverId = $post['driver_id'] ?? null;
|
||||
@@ -467,6 +499,20 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
|
||||
}
|
||||
$connection->send('OK');
|
||||
|
||||
// ── 7. Cache Driver Public Data ─────────────────────────
|
||||
} elseif ($action === 'cache_driver_public') {
|
||||
$driverId = $post['driver_id'] ?? null;
|
||||
$data = json_decode($post['data'] ?? '[]', true);
|
||||
|
||||
if ($driverId && $redis && !empty($data)) {
|
||||
$key = "driver:public:$driverId";
|
||||
$redis->hmset($key, $data);
|
||||
$redis->expire($key, 86400);
|
||||
$connection->send('OK');
|
||||
} else {
|
||||
$connection->send('Error');
|
||||
}
|
||||
|
||||
} else {
|
||||
$connection->send('Unknown action');
|
||||
}
|
||||
@@ -631,7 +677,9 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
|
||||
|
||||
if ($needHmset) {
|
||||
$eventBuffer[$driverId]['hmset'] = [
|
||||
'id' => $driverId, 'heading' => $heading, 'speed' => $speed, 'status' => $status, 'updated_at' => $now
|
||||
'id' => $driverId, 'lat' => $lat, 'lng' => $lng,
|
||||
'heading' => $heading, 'speed' => $speed,
|
||||
'status' => $status, 'updated_at' => $now
|
||||
];
|
||||
$state['speed'] = $speedMs;
|
||||
$state['heading'] = $heading;
|
||||
@@ -667,6 +715,13 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) {
|
||||
if ($needGeoadd) {
|
||||
$state['lat'] = $lat;
|
||||
$state['lng'] = $lng;
|
||||
// 🆕 تحديث الموقع في driver:public (حتى لو ما تغير speed/heading)
|
||||
if (!isset($eventBuffer[$driverId]['hmset'])) {
|
||||
$eventBuffer[$driverId]['hmset'] = [
|
||||
'id' => $driverId, 'lat' => $lat, 'lng' => $lng,
|
||||
'updated_at' => $now
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ class AppLink {
|
||||
static String get server => endPoint;
|
||||
|
||||
///=================ride==========================///
|
||||
///https://api.intaleq.xyz/siro/ride
|
||||
///https://jordan-siro.intaleqapp.com/backend
|
||||
static String get ride => '$server/ride';
|
||||
static String get rideServer {
|
||||
switch (currentCountry) {
|
||||
|
||||
@@ -51,7 +51,7 @@ Future<bool> onStart(ServiceInstance service) async {
|
||||
print("✅ Background Service: Socket Connected! ID: ${socket?.id}");
|
||||
|
||||
// 🆕 طلب أي رحلات انتظار فاتتنا أثناء انقطاع الخدمة
|
||||
socket.emit('get_pending_orders');
|
||||
socket!.emit('get_pending_orders');
|
||||
|
||||
if (service is AndroidServiceInstance) {
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,53 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.siro.socialmedia_bot"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.siro.socialmedia_bot"
|
||||
minSdk = 21
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
|
||||
// Coroutines – required by the bot services
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.siro.socialmedia_bot
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.siro.socialmedia_bot", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Socialmediabot">
|
||||
|
||||
<!-- Main launcher Activity -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Facebook Accessibility Service -->
|
||||
<service
|
||||
android:name=".social.facebook.FacebookBotService"
|
||||
android:label="Siro Facebook Bot"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/facebook_accessibility_service_config" />
|
||||
</service>
|
||||
|
||||
<!-- Instagram Accessibility Service -->
|
||||
<service
|
||||
android:name=".social.instagram.InstagramBotService"
|
||||
android:label="Siro Instagram Bot"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/instagram_accessibility_service_config" />
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.siro.socialmedia_bot
|
||||
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.siro.socialmedia_bot.databinding.ActivityMainBinding
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val logBuffer = StringBuilder()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.btnEnableAccessibility.setOnClickListener {
|
||||
openAccessibilitySettings()
|
||||
}
|
||||
|
||||
appendLog("[System] Siro Social Media Bot started.")
|
||||
appendLog("[System] Checking service status...")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateServiceStatus()
|
||||
}
|
||||
|
||||
private fun updateServiceStatus() {
|
||||
val isFbEnabled = isAccessibilityServiceEnabled("com.siro.socialmedia_bot/.social.facebook.FacebookBotService")
|
||||
val isIgEnabled = isAccessibilityServiceEnabled("com.siro.socialmedia_bot/.social.instagram.InstagramBotService")
|
||||
|
||||
if (isFbEnabled || isIgEnabled) {
|
||||
binding.statusDot.backgroundTintList = getColorStateList(android.R.color.holo_green_light)
|
||||
binding.tvStatus.text = "Accessibility Service Active"
|
||||
binding.btnEnableAccessibility.text = "✓ Service is Running"
|
||||
binding.btnEnableAccessibility.backgroundTintList = getColorStateList(android.R.color.holo_green_dark)
|
||||
} else {
|
||||
binding.statusDot.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFFFF4444.toInt())
|
||||
binding.tvStatus.text = "Accessibility Service Disabled"
|
||||
binding.btnEnableAccessibility.text = "Enable Accessibility Service"
|
||||
binding.btnEnableAccessibility.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFF1565C0.toInt())
|
||||
}
|
||||
|
||||
// Facebook dot
|
||||
if (isFbEnabled) {
|
||||
binding.fbDot.backgroundTintList = getColorStateList(android.R.color.holo_green_light)
|
||||
binding.tvFbState.text = "RUNNING"
|
||||
binding.tvFbState.setTextColor(0xFF00FF88.toInt())
|
||||
appendLog("[Facebook] Service is active and polling for tasks.")
|
||||
} else {
|
||||
binding.fbDot.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFF555555.toInt())
|
||||
binding.tvFbState.text = "DISABLED"
|
||||
binding.tvFbState.setTextColor(0xFF555555.toInt())
|
||||
}
|
||||
|
||||
// Instagram dot
|
||||
if (isIgEnabled) {
|
||||
binding.igDot.backgroundTintList = getColorStateList(android.R.color.holo_green_light)
|
||||
binding.tvIgState.text = "RUNNING"
|
||||
binding.tvIgState.setTextColor(0xFF00FF88.toInt())
|
||||
appendLog("[Instagram] Service is active and polling for tasks.")
|
||||
} else {
|
||||
binding.igDot.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFF555555.toInt())
|
||||
binding.tvIgState.text = "DISABLED"
|
||||
binding.tvIgState.setTextColor(0xFF555555.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAccessibilityServiceEnabled(serviceId: String): Boolean {
|
||||
val am = getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||
val enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
|
||||
return enabledServices.any { it.id == serviceId }
|
||||
}
|
||||
|
||||
private fun openAccessibilitySettings() {
|
||||
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||
startActivity(intent)
|
||||
appendLog("[System] Opened Accessibility settings. Enable both 'Siro Facebook Bot' and 'Siro Instagram Bot'.")
|
||||
}
|
||||
|
||||
private fun appendLog(msg: String) {
|
||||
val timestamp = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
logBuffer.append("[$timestamp] $msg\n")
|
||||
binding.tvLog.text = logBuffer.toString()
|
||||
binding.scrollLog.post { binding.scrollLog.fullScroll(android.view.View.FOCUS_DOWN) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.siro.socialmedia_bot.network
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
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 BOT_TOKEN = "YOUR_SECRET_BOT_TOKEN"
|
||||
|
||||
fun getTask(platform: String): JSONObject? {
|
||||
try {
|
||||
val url = URL("$BASE_URL?action=get_task&platform=$platform")
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
if (responseCode == 200) {
|
||||
val scanner = Scanner(connection.inputStream)
|
||||
val response = scanner.useDelimiter("\\A").next()
|
||||
scanner.close()
|
||||
|
||||
val json = JSONObject(response)
|
||||
if (json.getString("status") == "success" && !json.isNull("data")) {
|
||||
return json.getJSONObject("data")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun completeTask(taskId: Int, result: String) {
|
||||
postData("action=complete_task", "task_id=$taskId&result=$result")
|
||||
}
|
||||
|
||||
fun failTask(taskId: Int, errorMessage: String) {
|
||||
postData("action=fail_task", "task_id=$taskId&error_message=$errorMessage")
|
||||
}
|
||||
|
||||
fun logMessage(accountId: Int?, level: String, message: String) {
|
||||
val accIdParam = accountId?.let { "&account_id=$it" } ?: ""
|
||||
postData("action=log", "level=$level&message=$message$accIdParam")
|
||||
}
|
||||
|
||||
private fun postData(actionParam: String, postBody: String) {
|
||||
try {
|
||||
val url = URL("$BASE_URL?$actionParam")
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "POST"
|
||||
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
|
||||
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
connection.doOutput = true
|
||||
|
||||
val writer = OutputStreamWriter(connection.outputStream)
|
||||
writer.write(postBody)
|
||||
writer.flush()
|
||||
writer.close()
|
||||
|
||||
connection.responseCode // execute
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.siro.socialmedia_bot.social.facebook
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.util.Log
|
||||
import com.siro.socialmedia_bot.network.SocialBotClient
|
||||
import com.siro.socialmedia_bot.social.model.SocialTask
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class FacebookBotService : AccessibilityService() {
|
||||
|
||||
private val TAG = "FacebookBotService"
|
||||
private val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
private var isBotRunning = false
|
||||
private var currentTask: SocialTask? = null
|
||||
|
||||
private lateinit var navigator: FacebookNavigator
|
||||
private lateinit var commentReader: FacebookCommentReader
|
||||
private lateinit var commentPoster: FacebookCommentPoster
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
Log.d(TAG, "Facebook Accessibility Service Connected")
|
||||
|
||||
navigator = FacebookNavigator(this)
|
||||
commentReader = FacebookCommentReader(this)
|
||||
commentPoster = FacebookCommentPoster(this)
|
||||
|
||||
startTaskLoop()
|
||||
}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
// We handle logic manually via coroutines and window inspection,
|
||||
// but we can listen to events if needed for synchronization.
|
||||
}
|
||||
|
||||
override fun onInterrupt() {
|
||||
Log.d(TAG, "Service Interrupted")
|
||||
isBotRunning = false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun startTaskLoop() {
|
||||
if (isBotRunning) return
|
||||
isBotRunning = true
|
||||
|
||||
scope.launch {
|
||||
while (isBotRunning) {
|
||||
try {
|
||||
// Check for tasks
|
||||
val taskData = SocialBotClient.getTask("facebook")
|
||||
if (taskData != null) {
|
||||
currentTask = SocialTask(
|
||||
id = taskData.getInt("id"),
|
||||
type = taskData.getString("type"),
|
||||
targetUrl = if (taskData.isNull("target_url")) null else taskData.getString("target_url"),
|
||||
promptContext = if (taskData.isNull("prompt_context")) null else taskData.getString("prompt_context"),
|
||||
generatedComment = if (taskData.isNull("generated_comment")) null else taskData.getString("generated_comment")
|
||||
)
|
||||
|
||||
Log.d(TAG, "Received Task: ${currentTask?.type}")
|
||||
executeTask(currentTask!!)
|
||||
} else {
|
||||
Log.d(TAG, "No tasks available. Sleeping...")
|
||||
delay(60000) // Wait 1 minute before checking again
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in task loop", e)
|
||||
delay(30000) // Wait 30 seconds on error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun executeTask(task: SocialTask) {
|
||||
try {
|
||||
when (task.type) {
|
||||
"post_comment" -> {
|
||||
task.targetUrl?.let { url ->
|
||||
navigator.openUrl(url)
|
||||
delay(5000) // Wait for page load
|
||||
|
||||
task.generatedComment?.let { comment ->
|
||||
val success = commentPoster.postComment(comment)
|
||||
if (success) {
|
||||
SocialBotClient.completeTask(task.id, "Comment posted successfully")
|
||||
} else {
|
||||
SocialBotClient.failTask(task.id, "Failed to find comment box or post")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"read_posts" -> {
|
||||
// Logic to read posts and send to backend
|
||||
// ...
|
||||
SocialBotClient.completeTask(task.id, "Posts read successfully")
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown task type: ${task.type}")
|
||||
SocialBotClient.failTask(task.id, "Unknown task type")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
SocialBotClient.failTask(task.id, "Exception during execution: ${e.message}")
|
||||
}
|
||||
|
||||
// Add human-like delay between tasks
|
||||
val randomDelay = (10000..30000).random().toLong()
|
||||
delay(randomDelay)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.siro.socialmedia_bot.social.facebook
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.AccessibilityService.GestureResultCallback
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.os.Bundle
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class FacebookCommentPoster(private val service: AccessibilityService) {
|
||||
|
||||
suspend fun postComment(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")
|
||||
?: 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
|
||||
|
||||
// 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)
|
||||
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")
|
||||
if (sendBtn != null) {
|
||||
sendBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
delay(2000)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun findNodeByContentDescription(node: AccessibilityNodeInfo?, descPrefix: String): 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
|
||||
}
|
||||
|
||||
private fun findNodeByClassName(node: AccessibilityNodeInfo?, className: String): AccessibilityNodeInfo? {
|
||||
if (node == null) return null
|
||||
|
||||
if (node.className?.toString() == className) {
|
||||
return node
|
||||
}
|
||||
|
||||
for (i in 0 until node.childCount) {
|
||||
val child = findNodeByClassName(node.getChild(i), className)
|
||||
if (child != null) return child
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.siro.socialmedia_bot.social.facebook
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
class FacebookCommentReader(private val service: AccessibilityService) {
|
||||
|
||||
fun extractComments(): List<String> {
|
||||
val root = service.rootInActiveWindow ?: return emptyList()
|
||||
val comments = 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)
|
||||
|
||||
return comments
|
||||
}
|
||||
|
||||
private fun findTextNodes(node: AccessibilityNodeInfo?, comments: MutableList<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)
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until node.childCount) {
|
||||
findTextNodes(node.getChild(i), comments)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.siro.socialmedia_bot.social.facebook
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class FacebookNavigator(private val service: AccessibilityService) {
|
||||
|
||||
suspend fun openUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.setPackage("com.facebook.katana") // Open specifically in Facebook app
|
||||
|
||||
try {
|
||||
service.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
// Fallback if Facebook app is not installed
|
||||
intent.setPackage(null)
|
||||
service.startActivity(intent)
|
||||
}
|
||||
|
||||
// Wait for app to open
|
||||
delay(3000)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.siro.socialmedia_bot.social.instagram
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.util.Log
|
||||
import com.siro.socialmedia_bot.network.SocialBotClient
|
||||
import com.siro.socialmedia_bot.social.model.SocialTask
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Instagram Accessibility Service – mirrors the Facebook bot architecture
|
||||
* but targets the Instagram package (com.instagram.android).
|
||||
*/
|
||||
class InstagramBotService : AccessibilityService() {
|
||||
|
||||
private val TAG = "InstagramBotService"
|
||||
private val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
private var isBotRunning = false
|
||||
|
||||
private lateinit var navigator: InstagramNavigator
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
Log.d(TAG, "Instagram Accessibility Service Connected")
|
||||
navigator = InstagramNavigator(this)
|
||||
startTaskLoop()
|
||||
}
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||
// Observe events when needed for synchronization
|
||||
}
|
||||
|
||||
override fun onInterrupt() {
|
||||
Log.d(TAG, "Service Interrupted")
|
||||
isBotRunning = false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private fun startTaskLoop() {
|
||||
if (isBotRunning) return
|
||||
isBotRunning = true
|
||||
|
||||
scope.launch {
|
||||
while (isBotRunning) {
|
||||
try {
|
||||
val taskData = SocialBotClient.getTask("instagram")
|
||||
if (taskData != null) {
|
||||
val task = SocialTask(
|
||||
id = taskData.getInt("id"),
|
||||
type = taskData.getString("type"),
|
||||
targetUrl = if (taskData.isNull("target_url")) null else taskData.getString("target_url"),
|
||||
promptContext = if (taskData.isNull("prompt_context")) null else taskData.getString("prompt_context"),
|
||||
generatedComment = if (taskData.isNull("generated_comment")) null else taskData.getString("generated_comment")
|
||||
)
|
||||
Log.d(TAG, "Received Task: ${task.type}")
|
||||
executeTask(task)
|
||||
} else {
|
||||
Log.d(TAG, "No Instagram tasks. Sleeping...")
|
||||
delay(60_000)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in task loop", e)
|
||||
delay(30_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun executeTask(task: SocialTask) {
|
||||
try {
|
||||
when (task.type) {
|
||||
"post_comment" -> {
|
||||
task.targetUrl?.let { url ->
|
||||
navigator.openUrl(url)
|
||||
delay(5_000)
|
||||
task.generatedComment?.let { comment ->
|
||||
val success = navigator.postComment(comment)
|
||||
if (success) {
|
||||
SocialBotClient.completeTask(task.id, "IG comment posted")
|
||||
} else {
|
||||
SocialBotClient.failTask(task.id, "Could not find IG comment input")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown task type: ${task.type}")
|
||||
SocialBotClient.failTask(task.id, "Unknown task type")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
SocialBotClient.failTask(task.id, "Exception: ${e.message}")
|
||||
}
|
||||
delay((15_000..45_000).random().toLong()) // human-like cooldown
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.siro.socialmedia_bot.social.instagram
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* Handles navigation and interaction within the Instagram app.
|
||||
* Instagram's package name: com.instagram.android
|
||||
*/
|
||||
class InstagramNavigator(private val service: AccessibilityService) {
|
||||
|
||||
suspend fun openUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.setPackage("com.instagram.android")
|
||||
|
||||
try {
|
||||
service.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
intent.setPackage(null)
|
||||
service.startActivity(intent)
|
||||
}
|
||||
delay(4_000) // Wait for Instagram to load
|
||||
}
|
||||
|
||||
suspend fun postComment(commentText: String): Boolean {
|
||||
val root = service.rootInActiveWindow ?: return false
|
||||
|
||||
// On Instagram the comment field usually has a hint "Add a comment…"
|
||||
val inputNode = findNodeByHint(root, "Add a comment")
|
||||
?: findNodeByClassName(root, "android.widget.EditText")
|
||||
?: return false
|
||||
|
||||
inputNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
delay(1_000)
|
||||
|
||||
val args = Bundle()
|
||||
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, commentText)
|
||||
inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
|
||||
|
||||
delay((commentText.length * 40L).coerceAtMost(4_000L)) // typing simulation
|
||||
|
||||
// Look for "Post" button
|
||||
val postBtn = findNodeByText(service.rootInActiveWindow, "Post")
|
||||
if (postBtn != null) {
|
||||
postBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
delay(2_000)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ─── Utility helpers ───────────────────────────────────────────────────────
|
||||
|
||||
private fun findNodeByHint(node: AccessibilityNodeInfo?, hint: String): AccessibilityNodeInfo? {
|
||||
if (node == null) return null
|
||||
if (node.hintText?.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
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
for (i in 0 until node.childCount) {
|
||||
val found = findNodeByText(node.getChild(i), text)
|
||||
if (found != null) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findNodeByClassName(node: AccessibilityNodeInfo?, className: String): AccessibilityNodeInfo? {
|
||||
if (node == null) return null
|
||||
if (node.className?.toString() == className) return node
|
||||
for (i in 0 until node.childCount) {
|
||||
val found = findNodeByClassName(node.getChild(i), className)
|
||||
if (found != null) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.siro.socialmedia_bot.social.model
|
||||
|
||||
data class Account(
|
||||
val id: Int,
|
||||
val platform: String,
|
||||
val username: String
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.siro.socialmedia_bot.social.model
|
||||
|
||||
data class Comment(
|
||||
val author: String,
|
||||
val content: String,
|
||||
val time: String
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.siro.socialmedia_bot.social.model
|
||||
|
||||
data class SocialTask(
|
||||
val id: Int,
|
||||
val type: String, // 'join_group', 'read_posts', 'post_comment', 'share_link'
|
||||
val targetUrl: String?,
|
||||
val promptContext: String?,
|
||||
val generatedComment: String?
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#FFFFFF" />
|
||||
</shape>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:background="#0D0D0D"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<!-- Header -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🤖 Siro Social Bot"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFFFFF"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Social Media Automation Engine"
|
||||
android:textSize="13sp"
|
||||
android:textColor="#888888"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="32dp" />
|
||||
|
||||
<!-- Status Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#1A1A2E">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Service Status"
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/statusDot"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
android:background="@drawable/circle_indicator"
|
||||
android:backgroundTint="#FF4444"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStatus"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Accessibility Service Disabled"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="15sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Facebook Service Row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="12dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/fbDot"
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:background="@drawable/circle_indicator"
|
||||
android:backgroundTint="#555555"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Facebook Bot"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFbState"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="IDLE"
|
||||
android:textColor="#555555"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Instagram Service Row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/igDot"
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:background="@drawable/circle_indicator"
|
||||
android:backgroundTint="#555555"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Instagram Bot"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvIgState"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="IDLE"
|
||||
android:textColor="#555555"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Enable Accessibility Button -->
|
||||
<Button
|
||||
android:id="@+id/btnEnableAccessibility"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="Enable Accessibility Service"
|
||||
android:backgroundTint="#1565C0"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<!-- Logs Card -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Live Log"
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollLog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:background="#111111"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="[System] Bot initialized...\n"
|
||||
android:textColor="#00FF88"
|
||||
android:textSize="12sp"
|
||||
android:fontFamily="monospace" />
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Socialmediabot" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">SocialMedia Bot</string>
|
||||
<string name="accessibility_service_description">Social Media Bot Accessibility Service for automating Facebook interactions.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Socialmediabot" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older than API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagReportViewIds"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:canPerformGestures="true"
|
||||
android:description="@string/accessibility_service_description"
|
||||
android:notificationTimeout="100"
|
||||
android:packageNames="com.facebook.katana" />
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagReportViewIds"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:canPerformGestures="true"
|
||||
android:description="@string/accessibility_service_description"
|
||||
android:notificationTimeout="100"
|
||||
android:packageNames="com.instagram.android" />
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.siro.socialmedia_bot
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
@@ -0,0 +1,22 @@
|
||||
[versions]
|
||||
agp = "8.13.2"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.19.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.14.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#Sat Jul 04 18:14:32 EET 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,23 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "socialmedia-bot"
|
||||
include(":app")
|
||||