Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
+15
View File
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
+1
View File
@@ -0,0 +1 @@
/build
+61
View File
@@ -0,0 +1,61 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.siro.socialmedia_bot"
compileSdk = 36
defaultConfig {
applicationId = "com.siro.socialmedia_bot"
minSdk = 21
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// مضيف الباك إند في مكان واحد: تبديله عند نسخ البوت لعلامة أخرى
// (مثل انطلق على api.intaleqapp.com) يصير سطراً واحداً لا بحثاً في الكود.
buildConfigField("String", "BACKEND_HOST", "\"https://jordan-siro.intaleqapp.com\"")
// ⚠️ فحص هذا التوكن معطَّل على السيرفر حالياً (social_worker.php سطر 14
// و marketing_engine/index.php سطر 17) — نقطتا النهاية مفتوحتان فعلياً.
buildConfigField("String", "BOT_TOKEN", "\"YOUR_SECRET_BOT_TOKEN\"")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
// Coroutines – required by the bot services
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.siro.socialmedia_bot
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.siro.socialmedia_bot", appContext.packageName)
}
}
@@ -0,0 +1,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)
}
}
+5
View File
@@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}
+23
View File
@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
+22
View File
@@ -0,0 +1,22 @@
[versions]
agp = "8.13.2"
kotlin = "2.0.21"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
material = "1.12.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#Sat Jul 04 18:14:32 EET 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+23
View File
@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "socialmedia-bot"
include(":app")