feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
@@ -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,83 @@
<?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" />
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<!-- Required for Android 11+ to check and launch external apps like Facebook -->
<queries>
<package android:name="com.facebook.katana" />
<package android:name="com.facebook.lite" />
<package android:name="com.instagram.android" />
<package android:name="com.twitter.android" />
<package android:name="com.zhiliaoapp.musically" />
<package android:name="org.telegram.messenger" />
</queries>
<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>
<!-- Telegram Accessibility Service -->
<service
android:name=".social.telegram.TelegramBotService"
android:label="Siro Telegram 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/telegram_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,51 @@
package com.siro.socialmedia_bot.data
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import java.security.MessageDigest
class SeenPostDatabase(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "SeenPostsDB"
private const val DATABASE_VERSION = 1
private const val TABLE_SEEN_POSTS = "seen_posts"
private const val COLUMN_HASH = "hash"
}
override fun onCreate(db: SQLiteDatabase) {
val createTable = "CREATE TABLE $TABLE_SEEN_POSTS ($COLUMN_HASH TEXT PRIMARY KEY)"
db.execSQL(createTable)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS $TABLE_SEEN_POSTS")
onCreate(db)
}
fun isPostSeen(text: String): Boolean {
val hash = generateHash(text)
val db = this.readableDatabase
val cursor = db.rawQuery("SELECT 1 FROM $TABLE_SEEN_POSTS WHERE $COLUMN_HASH = ?", arrayOf(hash))
val exists = cursor.count > 0
cursor.close()
return exists
}
fun markPostAsSeen(text: String) {
val hash = generateHash(text)
val db = this.writableDatabase
val values = ContentValues().apply {
put(COLUMN_HASH, hash)
}
db.insertWithOnConflict(TABLE_SEEN_POSTS, null, values, SQLiteDatabase.CONFLICT_IGNORE)
}
private fun generateHash(input: String): String {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(input.toByteArray())
return digest.fold("") { str, it -> str + "%02x".format(it) }
}
}
@@ -0,0 +1,194 @@
package com.siro.socialmedia_bot.network
import com.siro.socialmedia_bot.BuildConfig
import org.json.JSONObject
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import java.util.Scanner
/**
* عميل الباك إند للبوت الاجتماعي.
*
* ⚠️ نقطتا نهاية لا واحدة — وخلط الاثنتين كان سبب تعطّل التعليق العضوي:
*
* - [ENGINE_URL] (`marketing_engine/index.php`) يخدم طابور `marketing_tasks`
* الذي تغذّيه `cron_insert_task.php` و`insert_autonomous_task.php`
* و`ContentWorkflow.php` بمهام `autonomous_scroll_and_reply`،
* ويملك `evaluate_posts` و`get_reports`.
* - [WORKER_URL] (`marketing_engine/social_worker.php`) يملك وحده
* `process_organic_post` (توليد التعليق عبر Gemini) و`log`.
*
* كان BASE_URL واحداً يشير إلى index.php، فكان `process_organic_post`
* و`log` يقعان على `default` فيعودان `{"status":"error"}` بكود 200 —
* أي **لا تعليق عضوي يُنشر أبداً وكل سجلات البوت تُبتلع صامتة**.
*/
object SocialBotClient {
// `val` لا `const val`: قيم BuildConfig ليست ثوابت وقت ترجمة بنظر كوتلن،
// و`const` عليها لا يترجم.
private val ENGINE_URL = "${BuildConfig.BACKEND_HOST}/backend/marketing_engine/index.php"
private val WORKER_URL = "${BuildConfig.BACKEND_HOST}/backend/marketing_engine/social_worker.php"
private val BOT_TOKEN = BuildConfig.BOT_TOKEN
private fun enc(value: String): String = URLEncoder.encode(value, "UTF-8")
fun getTask(platform: String): JSONObject? {
try {
val urlString = "$ENGINE_URL?action=get_task&platform=${enc(platform)}"
android.util.Log.d("SocialBotClient", "Requesting URL: $urlString")
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
val responseCode = connection.responseCode
android.util.Log.d("SocialBotClient", "Response Code: $responseCode")
if (responseCode == 200) {
val scanner = Scanner(connection.inputStream)
val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else ""
scanner.close()
android.util.Log.d("SocialBotClient", "Response Body: $response")
val json = JSONObject(response)
if (json.getString("status") == "success" && !json.isNull("data")) {
return json.getJSONObject("data")
}
} else {
val scanner = Scanner(connection.errorStream)
val errorResponse = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else ""
scanner.close()
android.util.Log.e("SocialBotClient", "Error Response Body: $errorResponse")
}
} catch (e: Exception) {
android.util.Log.e("SocialBotClient", "Exception in getTask: ${e.message}")
e.printStackTrace()
}
return null
}
fun completeTask(taskId: Int, result: String) {
postData(ENGINE_URL, "action=complete_task", "task_id=$taskId&result=${enc(result)}")
}
/**
* ملاحظة عقد: `index.php` يرد على النجاح بـ`status`+`message` **بلا حقل
* `data`** (يحفظ التقرير في `marketing_reports` ويُشعر الأدمن عبر FCM).
* لذا النجاح يُقاس بـ`status` لا بوجود `data`، وإلا بدا كل تقرير ناجح فشلاً.
*/
fun evaluatePosts(posts: List<String>, platform: String = "facebook"): JSONObject? {
try {
val urlString = "$ENGINE_URL?action=evaluate_posts&platform=${enc(platform)}"
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
connection.setRequestProperty("Content-Type", "application/json")
connection.doOutput = true
val jsonBody = JSONObject()
val jsonArray = org.json.JSONArray(posts)
jsonBody.put("posts", jsonArray)
val writer = OutputStreamWriter(connection.outputStream)
writer.write(jsonBody.toString())
writer.flush()
writer.close()
val responseCode = connection.responseCode
if (responseCode == 200) {
val scanner = Scanner(connection.inputStream)
val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else ""
scanner.close()
val json = JSONObject(response)
if (json.getString("status") == "success") {
return if (!json.isNull("data")) json.getJSONObject("data") else json
}
android.util.Log.e("SocialBotClient", "evaluatePosts rejected: $response")
} else {
android.util.Log.e("SocialBotClient", "evaluatePosts HTTP $responseCode")
}
} catch (e: Exception) {
android.util.Log.e("SocialBotClient", "Exception in evaluatePosts: ${e.message}")
e.printStackTrace()
}
return null
}
/** يخاطب [WORKER_URL] — `index.php` لا يعرف هذا الأمر إطلاقاً. */
fun processOrganicPost(deviceId: String, platform: String, postText: String, targetUrl: String): JSONObject? {
try {
val urlString = "$WORKER_URL?action=process_organic_post"
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
val params = "device_id=${enc(deviceId)}&platform=${enc(platform)}" +
"&post_text=${enc(postText)}&target_url=${enc(targetUrl)}"
connection.doOutput = true
val writer = OutputStreamWriter(connection.outputStream)
writer.write(params)
writer.flush()
writer.close()
val responseCode = connection.responseCode
if (responseCode == 200) {
val scanner = Scanner(connection.inputStream)
val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else ""
scanner.close()
val json = JSONObject(response)
if (json.getString("status") == "success" && !json.isNull("data")) {
return json.getJSONObject("data")
}
android.util.Log.e("SocialBotClient", "processOrganicPost rejected: $response")
} else {
android.util.Log.e("SocialBotClient", "processOrganicPost HTTP $responseCode")
}
} catch (e: Exception) {
android.util.Log.e("SocialBotClient", "Exception in processOrganicPost: ${e.message}")
e.printStackTrace()
}
return null
}
fun failTask(taskId: Int, errorMessage: String) {
postData(ENGINE_URL, "action=fail_task", "task_id=$taskId&error_message=${enc(errorMessage)}")
}
/** يخاطب [WORKER_URL] — يكتب في `social_logs`؛ `index.php` لا يعرف `log`. */
fun logMessage(accountId: Int?, level: String, message: String) {
val accIdParam = accountId?.let { "&account_id=$it" } ?: ""
postData(WORKER_URL, "action=log", "level=${enc(level)}&message=${enc(message)}$accIdParam")
}
private fun postData(endpoint: String, actionParam: String, postBody: String) {
try {
val url = URL("$endpoint?$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()
val responseCode = connection.responseCode
if (responseCode != 200) {
android.util.Log.e("SocialBotClient", "$actionParam HTTP $responseCode")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@@ -0,0 +1,210 @@
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(3600000) // Wait 1 hour 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")
}
}
}
}
"autonomous_scroll_and_reply" -> {
Log.d(TAG, "Starting autonomous mode. Opening Facebook app...")
navigator.openApp()
delay(3000)
val deviceId = android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID)
val totalIterations = 20
var iterations = 0
var consecutiveNoNewPosts = 0
var lastProcessedPostText = ""
while (iterations < totalIterations) {
Log.d(TAG, "Autonomous Iteration: ${iterations + 1} of $totalIterations")
// 1. Expand texts
commentReader.expandPostText()
delay(1000)
// 2. Read feed posts
val posts = commentReader.extractPostsAndComments()
val currentPost = posts.firstOrNull { it.length > 20 && it != lastProcessedPostText }
if (currentPost != null) {
Log.d(TAG, "Found valid post: \n$currentPost")
lastProcessedPostText = currentPost
consecutiveNoNewPosts = 0
// 3. Attempt to copy link
val linkCopied = navigator.copyPostLink()
if (linkCopied) {
// 4. Get copied link from clipboard
val clipboardManager = getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
val clipboardText = clipboardManager.primaryClip?.getItemAt(0)?.text?.toString() ?: ""
if (clipboardText.contains("http", ignoreCase = true)) {
Log.d(TAG, "Copied Link: $clipboardText")
// 5. Send to Server for organic processing
Log.d(TAG, "Sending to server for organic processing...")
val responseData = SocialBotClient.processOrganicPost(deviceId, "facebook", currentPost, clipboardText)
if (responseData != null && responseData.has("generated_comment")) {
val comment = responseData.getString("generated_comment")
Log.d(TAG, "Server returned comment: $comment")
// 6. Post the comment
if (commentReader.openComments()) {
delay(3000)
val posted = commentPoster.postComment(comment)
if (posted) {
Log.d(TAG, "Successfully posted organic comment!")
SocialBotClient.logMessage(null, "info", "Successfully posted organic comment on: $clipboardText")
}
navigator.goBack() // Close comments
delay(2000)
}
}
} else {
Log.w(TAG, "Copied text was not a URL: $clipboardText")
}
} else {
Log.w(TAG, "Failed to copy link for this post.")
}
} else {
consecutiveNoNewPosts++
Log.d(TAG, "No new posts found in this iteration. (Streak: $consecutiveNoNewPosts)")
if (consecutiveNoNewPosts >= 3) {
Log.d(TAG, "No new posts for 3 consecutive iterations. Breaking early.")
break
}
}
Log.d(TAG, "Scrolling down for more posts...")
navigator.scrollForward()
delay(4000) // wait for new posts to load
iterations++
}
SocialBotClient.completeTask(task.id, "Autonomous session completed.")
}
"read_posts" -> {
// Logic to read posts and send to backend
Log.d(TAG, "Starting to scan screen for posts/comments...")
val posts = commentReader.extractPostsAndComments()
if (posts.isNotEmpty()) {
Log.d(TAG, "===== FOUND ${posts.size} POSTS/COMMENTS =====")
for ((index, post) in posts.withIndex()) {
Log.d(TAG, "Post [${index + 1}]: $post")
}
Log.d(TAG, "===============================================")
// TODO: Send extracted posts to backend
SocialBotClient.completeTask(task.id, "Read ${posts.size} posts successfully")
} else {
Log.d(TAG, "No valid posts found on the screen.")
SocialBotClient.failTask(task.id, "No posts found")
}
}
else -> {
Log.w(TAG, "Unknown task type: ${task.type}")
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,140 @@
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 {
return performCommentPosting(commentText)
}
suspend fun postCommentOnSpecificPost(originalText: String, commentText: String): Boolean {
val root = service.rootInActiveWindow ?: return false
// 1. Find the specific post node
val postNode = findNodeByExactText(root, originalText)
if (postNode == null) {
android.util.Log.e("FacebookCommentPoster", "Could not find original post text on screen")
return false
}
// 2. Find the comment button associated with this post
// We go up to the parent container and look for a "Comment" or "تعليق" button
var commentBtn = findCommentButtonNearby(postNode)
if (commentBtn == null) {
// Fallback: just search the whole screen for the first comment button
commentBtn = findNodeByContentDescription(root, "Comment") ?: findNodeByContentDescription(root, "تعليق")
}
if (commentBtn != null) {
commentBtn.performAction(AccessibilityNodeInfo.ACTION_CLICK)
delay(3000) // wait for comment overlay to open
val success = performCommentPosting(commentText)
// Go back to feed
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
delay(2000)
return success
}
android.util.Log.e("FacebookCommentPoster", "Could not find comment button")
return false
}
private suspend fun performCommentPosting(commentText: String): Boolean {
val root = service.rootInActiveWindow ?: return false
val inputNode = findNodeByContentDescription(root, "Write a comment")
?: findNodeByContentDescription(root, "اكتب تعليق")
?: findNodeByClassName(root, "android.widget.EditText")
if (inputNode == null) return false
inputNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
delay(1000)
val arguments = Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, commentText)
inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
val randomTypingDelay = (commentText.length * 50).toLong().coerceAtMost(4000L)
delay(randomTypingDelay)
val sendBtn = findNodeByContentDescription(service.rootInActiveWindow, "Send")
?: findNodeByContentDescription(service.rootInActiveWindow, "إرسال")
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
}
private fun findNodeByExactText(node: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? {
if (node == null) return null
val nodeText = node.text?.toString()?.trim() ?: node.contentDescription?.toString()?.trim()
if (nodeText == text.trim()) {
return node
}
for (i in 0 until node.childCount) {
val child = findNodeByExactText(node.getChild(i), text)
if (child != null) return child
}
return null
}
private fun findCommentButtonNearby(postNode: AccessibilityNodeInfo): AccessibilityNodeInfo? {
// Go up the view hierarchy to find the container
var parent = postNode.parent
var depth = 0
while (parent != null && depth < 5) {
val btn = findNodeByContentDescription(parent, "Comment")
?: findNodeByContentDescription(parent, "تعليق")
if (btn != null) return btn
parent = parent.parent
depth++
}
return null
}
}
@@ -0,0 +1,204 @@
package com.siro.socialmedia_bot.social.facebook
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityNodeInfo
import com.siro.socialmedia_bot.data.SeenPostDatabase
class FacebookCommentReader(private val service: AccessibilityService) {
private val seenDb = SeenPostDatabase(service)
fun extractPostsAndComments(): List<String> {
val root = service.rootInActiveWindow ?: return emptyList()
val extractedTexts = mutableListOf<String>()
// Use a set to avoid duplicates as AccessibilityNodeInfo can be nested deeply
val uniqueTexts = mutableSetOf<String>()
findTextNodes(root, uniqueTexts)
extractedTexts.addAll(uniqueTexts)
// Print to Logcat for debugging purposes
for (text in extractedTexts) {
android.util.Log.d("ScrapedText", "Extracted: $text")
}
return extractedTexts
}
private fun findTextNodes(node: AccessibilityNodeInfo?, collectedTexts: MutableSet<String>) {
if (node == null) return
// Skip nodes that are off-screen or hidden to avoid reading background side-menus
if (!node.isVisibleToUser) return
val text = node.text?.toString() ?: node.contentDescription?.toString()
if (!text.isNullOrBlank()) {
val trimmedText = text.trim()
if (isValidPostText(trimmedText)) {
if (!seenDb.isPostSeen(trimmedText)) {
collectedTexts.add(trimmedText)
seenDb.markPostAsSeen(trimmedText)
}
}
}
for (i in 0 until node.childCount) {
findTextNodes(node.getChild(i), collectedTexts)
}
}
/**
* Filter out common Facebook UI elements and very short texts
* so we only send actual posts/comments to the backend.
*/
private fun isValidPostText(text: String): Boolean {
val trimmed = text.trim()
// 1. Ignore very short texts (less than 15 characters usually aren't full posts)
if (trimmed.length < 15) return false
// 2. Ignore common Facebook UI strings (English and Arabic)
val ignoreList = listOf(
"like", "comment", "share", "send", "write a comment",
"reply", "view more comments", "most relevant", "home",
"watch", "marketplace", "groups", "notifications", "menu",
"إعجاب", "تعليق", "مشاركة", "إرسال", "اكتب تعليقاً",
"رد", "عرض المزيد من التعليقات", "الأكثر صلة", "الصفحة الرئيسية",
"المجموعات", "الإشعارات", "القائمة", "Sponsored", "مُموَّل",
"انتقال إلى الملف الشخصي", "اكتب تعليقًا...", "بحث في المجموعات"
)
for (ignore in ignoreList) {
if (trimmed.equals(ignore, ignoreCase = true)) {
return false
}
}
// 2.5 Ignore UI patterns (Partial matches)
val ignorePatterns = listOf(
"علامة التبويب", "صورة ملف", "اقتراح", "لم تتم مشاهدتها",
"خيارات إضافية", "عرض المزيد من الخيارات", "تحديد صور أو مقاطع فيديو",
"إنشاء، اضغط ضغطًا مزدوجًا", "إزالة اقتراح", "عرض كل اقتراحات",
"زر أعجبني", "الزر مشاركة", "Action chip profile picture"
)
for (pattern in ignorePatterns) {
if (trimmed.contains(pattern, ignoreCase = true)) {
return false
}
}
// 3. Ignore texts that look like timestamps or metrics (e.g. "2 hrs", "10K Likes")
// Simplistic check: if it's short and ends with common time units
if (trimmed.length < 25) {
val timePatterns = listOf("hrs", "mins", "ساعات", "دقيقة", "دقائق", "أمس", "yesterday")
for (pattern in timePatterns) {
if (trimmed.contains(pattern, ignoreCase = true)) {
// It's likely a timestamp, not a post
return false
}
}
}
return true
}
fun expandPostText() {
val root = service.rootInActiveWindow ?: return
val expandNodes = mutableListOf<AccessibilityNodeInfo>()
findNodesByText(root, "عرض المزيد", expandNodes)
findNodesByText(root, "See more", expandNodes)
findNodesByText(root, "قراءة المزيد", expandNodes)
for (node in expandNodes) {
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
fun openComments(): Boolean {
val root = service.rootInActiveWindow ?: return false
val commentNodes = mutableListOf<AccessibilityNodeInfo>()
// Find nodes containing "تعليق" or "comment" in either text or description
findNodesByPartialText(root, "تعليق", commentNodes)
findNodesByPartialText(root, "Comment", commentNodes)
// Prioritize nodes that look like "5 تعليقات" or exact "تعليق"
commentNodes.sortBy {
val text = (it.text?.toString() ?: "") + (it.contentDescription?.toString() ?: "")
if (text.matches(Regex(".*\\d+.*(تعليق|comment).*"))) 0 else 1
}
val displayMetrics = service.resources.displayMetrics
val minAcceptableY = displayMetrics.heightPixels * 0.35f // Ignore top 35% of the screen (old posts)
for (node in commentNodes) {
val rect = android.graphics.Rect()
node.getBoundsInScreen(rect)
// If the comment button is too high on the screen, it belongs to the previous post!
if (rect.bottom < minAcceptableY) {
continue
}
val textStr = node.text?.toString() ?: ""
val descStr = node.contentDescription?.toString() ?: ""
val combined = "$textStr $descStr".toLowerCase()
// Avoid clicking the "write a comment" input box, the combined "Like, Comment, Share" container, or the Menu tab
if (combined.contains("اكتب") || combined.contains("write") ||
combined.contains("أعجبني") || combined.contains("like") ||
combined.contains("مشاركة") || combined.contains("share") ||
combined.contains("قائمة") || combined.contains("menu")) {
continue
}
// Try clicking the node
if (node.isClickable && node.performAction(AccessibilityNodeInfo.ACTION_CLICK)) return true
// Try clicking its parent (often icons are inside clickable containers)
// Limit depth to 1 to avoid clicking massive screen containers like the main navigation bar
var parent = node.parent
var depth = 0
while (parent != null && depth < 1) {
if (parent.isClickable && parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
return true
}
parent = parent.parent
depth++
}
}
return false
}
private fun findNodesByText(node: AccessibilityNodeInfo?, target: String, list: MutableList<AccessibilityNodeInfo>) {
if (node == null) return
if (!node.isVisibleToUser) return
val textStr = node.text?.toString() ?: ""
val descStr = node.contentDescription?.toString() ?: ""
if (textStr.equals(target, ignoreCase = true) || descStr.equals(target, ignoreCase = true)) {
list.add(node)
}
for (i in 0 until node.childCount) {
findNodesByText(node.getChild(i), target, list)
}
}
private fun findNodesByPartialText(node: AccessibilityNodeInfo?, target: String, list: MutableList<AccessibilityNodeInfo>) {
if (node == null) return
if (!node.isVisibleToUser) return
val textStr = node.text?.toString() ?: ""
val descStr = node.contentDescription?.toString() ?: ""
if (textStr.contains(target, ignoreCase = true) || descStr.contains(target, ignoreCase = true)) {
list.add(node)
}
for (i in 0 until node.childCount) {
findNodesByPartialText(node.getChild(i), target, list)
}
}
}
@@ -0,0 +1,320 @@
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.addFlags(Intent.FLAG_ACTIVITY_CLEAR_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)
}
suspend fun openApp() {
// Kill the app first so it restarts with a fresh feed
val am = service.getSystemService(android.content.Context.ACTIVITY_SERVICE) as android.app.ActivityManager
am.killBackgroundProcesses("com.facebook.katana")
am.killBackgroundProcesses("com.facebook.lite")
kotlinx.coroutines.delay(1000)
var launchIntent = service.packageManager.getLaunchIntentForPackage("com.facebook.katana")
if (launchIntent == null) {
launchIntent = service.packageManager.getLaunchIntentForPackage("com.facebook.lite")
}
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
service.startActivity(launchIntent)
delay(8000) // Wait 8 seconds for Facebook to load completely
// Explicitly click the Home tab to ensure we are on the main feed
val root = service.rootInActiveWindow
if (root != null) {
val homeTab = findNodeByContentDescription(root, "الرئيسية") ?: findNodeByContentDescription(root, "Home")
if (homeTab != null) {
homeTab.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
delay(3000)
}
}
// Try to navigate to Groups Tab automatically (if available)
openGroupsTab()
} else {
android.util.Log.e("FacebookNavigator", "Facebook app is not installed.")
}
}
private suspend fun openGroupsTab() {
val root = service.rootInActiveWindow ?: return
// Search for Groups tab
val groupsTab = findNodeByContentDescription(root, "المجموعات")
?: findNodeByContentDescription(root, "Groups")
if (groupsTab != null) {
groupsTab.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
delay(4000) // wait for Groups tab to load
}
}
suspend fun scrollForward(): Boolean {
var success = false
// DOM scroll attempts — refetch root each iteration to avoid stale nodes
for (i in 0 until 3) {
val root = service.rootInActiveWindow ?: continue
if (performDomScroll(root)) {
success = true
}
kotlinx.coroutines.delay(1000)
}
if (!success) {
android.util.Log.d("FacebookNavigator", "DOM Scroll failed, using Gesture Fallback!")
kotlinx.coroutines.delay(800)
// More attempts with moderate delay to let Facebook process each scroll
for (i in 0 until 4) {
success = performGestureScroll()
kotlinx.coroutines.delay(1200)
}
}
return success
}
private suspend fun performGestureScroll(): Boolean = kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
val displayMetrics = service.resources.displayMetrics
val middleX = displayMetrics.widthPixels / 2f
// Moderate swipe to avoid accidental pull-to-refresh; longer duration for reliability
val startY = displayMetrics.heightPixels * 0.75f
val endY = displayMetrics.heightPixels * 0.35f
val path = android.graphics.Path()
path.moveTo(middleX, startY)
path.lineTo(middleX, endY)
val gesture = android.accessibilityservice.GestureDescription.Builder()
.addStroke(android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 1200))
.build()
val dispatched = service.dispatchGesture(gesture, object : AccessibilityService.GestureResultCallback() {
override fun onCompleted(gestureDescription: android.accessibilityservice.GestureDescription?) {
if (continuation.isActive) continuation.resume(true, null)
}
override fun onCancelled(gestureDescription: android.accessibilityservice.GestureDescription?) {
if (continuation.isActive) continuation.resume(false, null)
}
}, null)
if (!dispatched) {
if (continuation.isActive) continuation.resume(false, null)
}
}
suspend fun goBack() {
service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
kotlinx.coroutines.delay(2000) // Wait for bottom sheet to close
}
private fun performDomScroll(node: android.view.accessibility.AccessibilityNodeInfo?): Boolean {
if (node == null) return false
val candidates = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
val displayMetrics = service.resources.displayMetrics
val screenWidth = displayMetrics.widthPixels
val screenHeight = displayMetrics.heightPixels
fun collectCandidates(n: android.view.accessibility.AccessibilityNodeInfo) {
val className = n.className?.toString() ?: ""
val isHorizontal = className.contains("ViewPager", ignoreCase = true) ||
className.contains("Horizontal", ignoreCase = true)
if (!isHorizontal) {
val rect = android.graphics.Rect()
n.getBoundsInScreen(rect)
// Main feed: vertical, tall, spans full screen width.
// isScrollable is deliberately NOT required — Facebook often sets it to false.
if (rect.height() > rect.width() &&
rect.height() > screenHeight * 0.3f &&
rect.width() >= screenWidth * 0.85f) {
candidates.add(n)
}
}
for (i in 0 until n.childCount) {
n.getChild(i)?.let { collectCandidates(it) }
}
}
collectCandidates(node)
// Prefer RecyclerView/ListView, then by largest area
candidates.sortWith(Comparator { a, b ->
fun isFeedCandidate(c: android.view.accessibility.AccessibilityNodeInfo): Boolean {
val cn = c.className?.toString() ?: ""
return cn.contains("RecyclerView", ignoreCase = true) ||
cn.contains("ListView", ignoreCase = true)
}
val aPref = isFeedCandidate(a)
val bPref = isFeedCandidate(b)
if (aPref != bPref) return@Comparator if (aPref) -1 else 1
val aRect = android.graphics.Rect().also { a.getBoundsInScreen(it) }
val bRect = android.graphics.Rect().also { b.getBoundsInScreen(it) }
(bRect.width() * bRect.height()).compareTo(aRect.width() * aRect.height())
})
for (candidate in candidates) {
try {
val scrolled = candidate.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
if (scrolled) return true
} catch (_: Exception) {
// performAction may throw on non-scrollable nodes
}
}
return false
}
private fun findNodeByContentDescription(node: android.view.accessibility.AccessibilityNodeInfo?, descPrefix: String): android.view.accessibility.AccessibilityNodeInfo? {
if (node == null) return null
if (node.contentDescription?.startsWith(descPrefix, ignoreCase = true) == true ||
node.text?.startsWith(descPrefix, ignoreCase = true) == true) {
return node
}
for (i in 0 until node.childCount) {
val child = findNodeByContentDescription(node.getChild(i), descPrefix)
if (child != null) return child
}
return null
}
suspend fun copyPostLink(): Boolean {
val root = service.rootInActiveWindow ?: return false
// 1. Click "Share" (مشاركة)
val shareNodes = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
findNodesByText(root, "مشاركة", shareNodes)
findNodesByText(root, "Share", shareNodes)
if (shareNodes.isEmpty()) {
android.util.Log.d("FacebookNavigator", "Share button not found")
return false
}
// Click the first valid Share button
val shareBtn = shareNodes.first()
shareBtn.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
kotlinx.coroutines.delay(2000) // Wait for bottom sheet
// 2. Look for "Copy Link" (نسخ الرابط) in the bottom sheet
val newRoot = service.rootInActiveWindow ?: return false
val copyNodes = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
findNodesByText(newRoot, "نسخ الرابط", copyNodes)
findNodesByText(newRoot, "Copy link", copyNodes)
if (copyNodes.isEmpty()) {
android.util.Log.d("FacebookNavigator", "Copy Link button not found in sheet")
goBack() // close sheet
return false
}
// Click Copy Link
val copyBtn = copyNodes.first()
copyBtn.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
kotlinx.coroutines.delay(1000)
return true
}
private fun findNodesByText(node: android.view.accessibility.AccessibilityNodeInfo?, text: String, list: MutableList<android.view.accessibility.AccessibilityNodeInfo>) {
if (node == null) return
val nodeText = (node.text?.toString() ?: "") + " " + (node.contentDescription?.toString() ?: "")
if (nodeText.contains(text, ignoreCase = true)) {
list.add(node)
}
for (i in 0 until node.childCount) {
findNodesByText(node.getChild(i), text, list)
}
}
suspend fun createPost(text: String): Boolean {
val root = service.rootInActiveWindow ?: return false
// 1. Find "What's on your mind?" or "Write something..."
val inputBoxNodes = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
findNodesByText(root, "بم تفكر؟", inputBoxNodes)
findNodesByText(root, "What's on your mind", inputBoxNodes)
findNodesByText(root, "اكتب شيئًا", inputBoxNodes)
findNodesByText(root, "Write something", inputBoxNodes)
if (inputBoxNodes.isEmpty()) {
android.util.Log.d("FacebookNavigator", "Create post box not found")
return false
}
val inputBox = inputBoxNodes.first()
inputBox.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
kotlinx.coroutines.delay(3000) // Wait for Create Post screen to open
// 2. Find the EditText field
val newRoot = service.rootInActiveWindow ?: return false
val editTexts = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
findEditTexts(newRoot, editTexts)
if (editTexts.isEmpty()) {
android.util.Log.d("FacebookNavigator", "EditText not found in Create Post screen")
return false
}
// Paste the text
val editText = editTexts.first()
val arguments = android.os.Bundle()
arguments.putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
editText.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
kotlinx.coroutines.delay(1000)
// 3. Click "Post" (نشر)
val postRoot = service.rootInActiveWindow ?: return false
val postBtns = mutableListOf<android.view.accessibility.AccessibilityNodeInfo>()
findNodesByText(postRoot, "نشر", postBtns)
findNodesByText(postRoot, "Post", postBtns)
if (postBtns.isNotEmpty()) {
postBtns.first().performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
kotlinx.coroutines.delay(4000) // Wait for upload
return true
}
return false
}
private fun findEditTexts(node: android.view.accessibility.AccessibilityNodeInfo?, list: MutableList<android.view.accessibility.AccessibilityNodeInfo>) {
if (node == null) return
if (node.className?.toString()?.contains("EditText") == true) {
list.add(node)
}
for (i in 0 until node.childCount) {
findEditTexts(node.getChild(i), list)
}
}
}
@@ -0,0 +1,174 @@
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")
}
}
}
}
"autonomous_scroll_and_reply" -> {
Log.d(TAG, "Starting autonomous mode for Instagram...")
if (task.targetUrl != null) {
navigator.openUrl(task.targetUrl)
} else {
navigator.openApp()
}
delay(3000)
val allCollectedTexts = mutableSetOf<String>()
val totalIterations = 15 // Roughly 3-4 minutes of scrolling
var iterations = 0
var consecutiveNoNewPosts = 0
while (iterations < totalIterations) {
Log.d(TAG, "Autonomous Iteration: ${iterations + 1} of $totalIterations")
val initialSize = allCollectedTexts.size
// 1. Read feed posts and comments on screen
val posts = navigator.extractPostsAndComments().map { "[FEED]: $it" }
// Add to our global collection (Set avoids duplicates)
allCollectedTexts.addAll(posts)
if (navigator.openComments()) {
Log.d(TAG, "Opened comments section...")
delay(3000)
var comments = navigator.extractPostsAndComments().map { "[COMMENT]: $it" }
allCollectedTexts.addAll(comments)
// Scroll down inside the comments list to read more
for (i in 1..2) {
Log.d(TAG, "Scrolling inside comments (Scroll $i/2)")
navigator.scrollForward()
delay(2000)
comments = navigator.extractPostsAndComments().map { "[COMMENT]: $it" }
allCollectedTexts.addAll(comments)
}
// Go back to the feed
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
delay(2000)
}
Log.d(TAG, "Collected ${posts.size} texts in this iteration. Total unique so far: ${allCollectedTexts.size}")
if (allCollectedTexts.size == initialSize) {
consecutiveNoNewPosts++
Log.d(TAG, "No new posts found in this iteration. (Streak: $consecutiveNoNewPosts)")
if (consecutiveNoNewPosts >= 5) {
Log.d(TAG, "No new posts for 5 consecutive iterations. Reached bottom or stuck. Breaking early.")
break
}
} else {
consecutiveNoNewPosts = 0
}
Log.d(TAG, "Scrolling down for more posts...")
navigator.scrollForward()
delay(3000) // wait for new posts to load
iterations++
}
// Send everything to Gemini AT ONCE to save API costs
if (allCollectedTexts.isNotEmpty()) {
Log.d(TAG, "Finished scrolling. Sending ${allCollectedTexts.size} texts to backend (platform=instagram)...")
// The endpoint should handle this dynamically if we pass platform to SocialBotClient
// Wait, SocialBotClient currently uses a hardcoded url or passes platform?
// Let's assume evaluatePosts can take platform or defaults to facebook. We will need to update evaluatePosts in SocialBotClient.
SocialBotClient.evaluatePosts(allCollectedTexts.toList(), "instagram")
}
SocialBotClient.completeTask(task.id, "Autonomous session completed. Scraped ${allCollectedTexts.size} items.")
}
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,210 @@
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
import com.siro.socialmedia_bot.data.SeenPostDatabase
/**
* Handles navigation and interaction within the Instagram app.
* Instagram's package name: com.instagram.android
*/
class InstagramNavigator(private val service: AccessibilityService) {
private val seenDb = SeenPostDatabase(service)
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 openApp() {
val intent = service.packageManager.getLaunchIntentForPackage("com.instagram.android")
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
service.startActivity(intent)
delay(5_000)
} else {
// Instagram not installed
}
}
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 ───────────────────────────────────────────────────────
fun scrollForward(): Boolean {
val root = service.rootInActiveWindow ?: return false
val listNode = findScrollableNode(root)
if (listNode != null) {
val scrolled = listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
if (scrolled) return true
}
// Fallback to gesture swipe
val displayMetrics = service.resources.displayMetrics
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
val path = android.graphics.Path()
path.moveTo(width / 2f, height * 0.8f)
path.lineTo(width / 2f, height * 0.2f)
val gesture = android.accessibilityservice.GestureDescription.Builder()
.addStroke(android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 500))
.build()
return service.dispatchGesture(gesture, null, null)
}
fun extractPostsAndComments(): List<String> {
val root = service.rootInActiveWindow ?: return emptyList()
val extractedTexts = mutableSetOf<String>()
extractTextRecursively(root, extractedTexts)
return extractedTexts.toList()
}
private fun extractTextRecursively(node: AccessibilityNodeInfo?, results: MutableSet<String>) {
if (node == null) return
// Only target TextViews
if (node.className?.toString() == "android.widget.TextView") {
val text = node.text?.toString()?.trim()
val contentDesc = node.contentDescription?.toString()?.trim()
val validText = text ?: contentDesc
// Filter out short UI elements, timestamps, likes, etc.
if (!validText.isNullOrEmpty() && validText.length >= 10 && !validText.contains(Regex("^[0-9]+ likes$"))) {
if (!seenDb.isPostSeen(validText)) {
android.util.Log.d("ScrapedText", "Extracted: $validText")
results.add(validText)
seenDb.markPostAsSeen(validText)
}
}
}
for (i in 0 until node.childCount) {
extractTextRecursively(node.getChild(i), results)
}
}
private fun findScrollableNode(node: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
if (node == null) return null
if (node.isScrollable && (node.className?.toString()?.contains("RecyclerView") == true || node.className?.toString()?.contains("ListView") == true)) {
return node
}
for (i in 0 until node.childCount) {
val found = findScrollableNode(node.getChild(i))
if (found != null) return found
}
return null
}
fun openComments(): Boolean {
val root = service.rootInActiveWindow ?: return false
val commentNodes = mutableListOf<AccessibilityNodeInfo>()
findNodesByPartialText(root, "تعليق", commentNodes)
findNodesByPartialText(root, "Comment", commentNodes)
for (node in commentNodes) {
if (node.isClickable && node.performAction(AccessibilityNodeInfo.ACTION_CLICK)) return true
var parent = node.parent
var depth = 0
while (parent != null && depth < 2) {
if (parent.isClickable && parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)) {
return true
}
parent = parent.parent
depth++
}
}
return false
}
private fun findNodesByPartialText(node: AccessibilityNodeInfo?, target: String, list: MutableList<AccessibilityNodeInfo>) {
if (node == null) return
if (!node.isVisibleToUser) return
val textStr = node.text?.toString() ?: ""
val descStr = node.contentDescription?.toString() ?: ""
if (textStr.contains(target, ignoreCase = true) || descStr.contains(target, ignoreCase = true)) {
list.add(node)
}
for (i in 0 until node.childCount) {
findNodesByPartialText(node.getChild(i), target, list)
}
}
private fun findNodeByHint(node: AccessibilityNodeInfo?, hint: String): AccessibilityNodeInfo? {
if (node == null) return null
if (node.hintText?.toString()?.contains(hint, ignoreCase = true) == true) return node
for (i in 0 until node.childCount) {
val found = findNodeByHint(node.getChild(i), hint)
if (found != null) return found
}
return null
}
private fun findNodeByText(node: AccessibilityNodeInfo?, text: String): AccessibilityNodeInfo? {
if (node == null) return null
if (node.text?.toString()?.equals(text, ignoreCase = true) == true ||
node.contentDescription?.toString()?.equals(text, ignoreCase = true) == true) return node
for (i in 0 until node.childCount) {
val found = findNodeByText(node.getChild(i), text)
if (found != null) return found
}
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,128 @@
package com.siro.socialmedia_bot.social.telegram
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 TelegramBotService : AccessibilityService() {
private val TAG = "TelegramBotService"
private val scope = CoroutineScope(Dispatchers.IO + Job())
private var isBotRunning = false
private lateinit var navigator: TelegramNavigator
override fun onServiceConnected() {
super.onServiceConnected()
Log.d(TAG, "Telegram Accessibility Service Connected")
navigator = TelegramNavigator(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("telegram")
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 Telegram 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) {
"autonomous_scroll_and_reply" -> {
Log.d(TAG, "Starting autonomous mode for Telegram...")
if (task.targetUrl != null) {
navigator.openChannel(task.targetUrl)
} else {
navigator.openApp()
}
delay(3000)
val allCollectedTexts = mutableSetOf<String>()
val totalIterations = 15
var iterations = 0
var consecutiveNoNewPosts = 0
while (iterations < totalIterations) {
Log.d(TAG, "Autonomous Iteration: ${iterations + 1} of $totalIterations")
val initialSize = allCollectedTexts.size
val posts = navigator.extractPostsAndComments()
allCollectedTexts.addAll(posts)
Log.d(TAG, "Collected ${posts.size} texts in this iteration. Total unique so far: ${allCollectedTexts.size}")
if (allCollectedTexts.size == initialSize) {
consecutiveNoNewPosts++
Log.d(TAG, "No new posts found in this iteration. (Streak: $consecutiveNoNewPosts)")
if (consecutiveNoNewPosts >= 3) {
break
}
} else {
consecutiveNoNewPosts = 0
}
Log.d(TAG, "Scrolling up for more posts...")
navigator.scrollForward()
delay(2500)
iterations++
}
if (allCollectedTexts.isNotEmpty()) {
Log.d(TAG, "Finished scrolling. Sending ${allCollectedTexts.size} texts to backend (platform=telegram)...")
SocialBotClient.evaluatePosts(allCollectedTexts.toList(), "telegram")
}
SocialBotClient.completeTask(task.id, "Autonomous session completed. Scraped ${allCollectedTexts.size} items.")
}
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..30_000).random().toLong())
}
}
@@ -0,0 +1,113 @@
package com.siro.socialmedia_bot.social.telegram
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
import com.siro.socialmedia_bot.data.SeenPostDatabase
/**
* Handles navigation and interaction within the Telegram app.
* Telegram's package name: org.telegram.messenger
*/
class TelegramNavigator(private val service: AccessibilityService) {
private val seenDb = SeenPostDatabase(service)
suspend fun openApp() {
val intent = service.packageManager.getLaunchIntentForPackage("org.telegram.messenger")
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
service.startActivity(intent)
delay(5_000)
} else {
// Telegram not installed
}
}
suspend fun openChannel(channelLink: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(channelLink))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.setPackage("org.telegram.messenger")
try {
service.startActivity(intent)
} catch (e: Exception) {
intent.setPackage(null)
service.startActivity(intent)
}
delay(4_000)
}
fun scrollForward(): Boolean {
val root = service.rootInActiveWindow ?: return false
val listNode = findScrollableNode(root)
if (listNode != null) {
val scrolled = listNode.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)
// Telegram might scroll backward if it's reading top to bottom in chat, but we usually want older posts
// so scroll backward is actually ACTION_SCROLL_BACKWARD for loading old messages.
if (scrolled) return true
}
val displayMetrics = service.resources.displayMetrics
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
val path = android.graphics.Path()
path.moveTo(width / 2f, height * 0.2f) // swipe down to go up in chat
path.lineTo(width / 2f, height * 0.8f)
val gesture = android.accessibilityservice.GestureDescription.Builder()
.addStroke(android.accessibilityservice.GestureDescription.StrokeDescription(path, 0, 500))
.build()
return service.dispatchGesture(gesture, null, null)
}
fun extractPostsAndComments(): List<String> {
val root = service.rootInActiveWindow ?: return emptyList()
val extractedTexts = mutableSetOf<String>()
extractTextRecursively(root, extractedTexts)
return extractedTexts.toList()
}
private fun extractTextRecursively(node: AccessibilityNodeInfo?, results: MutableSet<String>) {
if (node == null) return
// Target TextViews
if (node.className?.toString() == "android.widget.TextView") {
val text = node.text?.toString()?.trim()
val contentDesc = node.contentDescription?.toString()?.trim()
val validText = text ?: contentDesc
// Filter short words
if (!validText.isNullOrEmpty() && validText.length > 20) {
if (!seenDb.isPostSeen(validText)) {
android.util.Log.d("ScrapedText", "Extracted: $validText")
results.add(validText)
seenDb.markPostAsSeen(validText)
}
}
}
for (i in 0 until node.childCount) {
extractTextRecursively(node.getChild(i), results)
}
}
private fun findScrollableNode(node: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
if (node == null) return null
if (node.isScrollable && (node.className?.toString()?.contains("RecyclerView") == true || node.className?.toString()?.contains("ListView") == true || node.className?.toString()?.contains("ScrollView") == true)) {
return node
}
for (i in 0 until node.childCount) {
val found = findScrollableNode(node.getChild(i))
if (found != null) return found
}
return null
}
}
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

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,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="org.telegram.messenger" />
@@ -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)
}
}