Update: 2026-07-05 00:21:29
This commit is contained in:
@@ -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"
|
||||
|
||||
+53
@@ -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")
|
||||
|
||||
+76
@@ -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
|
||||
|
||||
+128
@@ -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())
|
||||
}
|
||||
}
|
||||
+105
@@ -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" />
|
||||
Reference in New Issue
Block a user