Update: 2026-07-05 00:21:29

This commit is contained in:
Hamza-Ayed
2026-07-05 00:21:29 +03:00
parent 35c0a6680c
commit 5cf809e0cb
11 changed files with 418 additions and 9 deletions
@@ -13,6 +13,7 @@
<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
@@ -63,6 +64,20 @@
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>
@@ -54,9 +54,9 @@ object SocialBotClient {
postData("action=complete_task", "task_id=$taskId&result=$result")
}
fun evaluatePosts(posts: List<String>): JSONObject? {
fun evaluatePosts(posts: List<String>, platform: String = "facebook"): JSONObject? {
try {
val urlString = "$BASE_URL?action=evaluate_posts"
val urlString = "$BASE_URL?action=evaluate_posts&platform=$platform"
val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
@@ -87,6 +87,59 @@ class InstagramBotService : AccessibilityService() {
}
}
}
"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()
// Add to our global collection (Set avoids duplicates)
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) {
Log.d(TAG, "No new posts for 3 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")
@@ -27,6 +27,17 @@ class InstagramNavigator(private val service: AccessibilityService) {
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
@@ -56,6 +67,71 @@ class InstagramNavigator(private val service: AccessibilityService) {
// ─── 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 > 20 && !validText.contains(Regex("^[0-9]+ likes$"))) {
results.add(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
}
private fun findNodeByHint(node: AccessibilityNodeInfo?, hint: String): AccessibilityNodeInfo? {
if (node == null) return null
if (node.hintText?.toString()?.contains(hint, ignoreCase = true) == true) return node
@@ -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,105 @@
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
/**
* Handles navigation and interaction within the Telegram app.
* Telegram's package name: org.telegram.messenger
*/
class TelegramNavigator(private val service: AccessibilityService) {
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) {
results.add(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,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" />