fix(bots): توجيه السوشال بوت لنقطة النهاية الصحيحة — التعليق العضوي كان ميتاً

السبب الجذري: BASE_URL واحد يشير إلى marketing_engine/index.php، بينما
`process_organic_post` و`log` موجودان في social_worker.php وحده. فكانا
يقعان على `default` فيعود {"status":"error"} بكود HTTP 200 — بلا استثناء
ولا سجل. أثره: processOrganicPost يعود null فيفشل شرط generated_comment في
FacebookBotService:137 → لا تعليق عضوي يُنشر أبداً، وكل logMessage يُبتلع
صامتاً فلم يظهر العطل في أي سجل. (تعليق العميل سطر 10 كان يذكر
social_worker.php أصلاً — الرابط هو ما خالف النية.)

- توجيه لكل أمر حسب موضعه: index.php لـ get_task/complete_task/fail_task/
  evaluate_posts · social_worker.php لـ process_organic_post/log
- evaluate_posts: index.php يرد بلا حقل `data`، والعميل كان يشترطه فيعود
  null في كل نجاح. النجاح صار يُقاس بـ status
- ترميز URL لكل المعاملات: رسالة خطأ فيها & أو # كانت تشقّ جسم
  x-www-form-urlencoded وتفسد صفّ المهمة
- كل رد غير 200 أو status != success يُسجَّل في Logcat بدل الصمت
- المضيف إلى BuildConfig.BACKEND_HOST في البوتين (gradle) — لا نطاق مشفّر
  في الكود، فتبديله لأي علامة أخرى سطر واحد
- val لا const val لقيم BuildConfig (ليست ثوابت وقت ترجمة في كوتلن)

لم يُبنَ بعد. لم يُمسّ أي PHP: الباك إند حيّ.
ما زال مفتوحاً بقرار المالك: (1) فحص X-Bot-Token معطَّل بتعليق في
social_worker.php:14 و index.php:17 والتوكن placeholder → النقطتان
مفتوحتان؛ (2) طابور social_tasks الذي يغذّيه schedule_manager بلا مستهلك.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:06:08 +03:00
co-authored by Claude Opus 5
parent 4d2beaae5e
commit ecfe756849
4 changed files with 78 additions and 27 deletions
+3
View File
@@ -17,6 +17,9 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BOT_SECRET_KEY", "\"SIRO_BOT_SUPER_SECRET_123\"") buildConfigField("String", "BOT_SECRET_KEY", "\"SIRO_BOT_SUPER_SECRET_123\"")
// مضيف الباك إند في مكان واحد: تبديله عند نسخ البوت لعلامة أخرى
// (مثل انطلق على api.intaleqapp.com) يصير سطراً واحداً لا بحثاً في الكود.
buildConfigField("String", "BACKEND_HOST", "\"https://jordan-siro.intaleqapp.com\"")
} }
buildTypes { buildTypes {
@@ -25,9 +25,11 @@ class WorkerClient(private val context: Context) {
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN_DEVICE" Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN_DEVICE"
} }
// Change this to your actual server domain // المضيف من BuildConfig.BACKEND_HOST (app/build.gradle.kts) — لا يُشفَّر هنا،
private val BASE_URL = "https://jordan-siro.intaleqapp.com/backend/bot/standalone_worker.php" // حتى يكون تبديله عند نسخ البوت لعلامة أخرى سطراً واحداً في gradle.
// For local testing use: "http://10.0.2.2:8000/standalone_worker.php" // للتجريب المحلي: اضبط BACKEND_HOST على "http://10.0.2.2:8000" مع تقديم
// نفس المسار /backend/bot/ من جذر السيرفر المحلي.
private val BASE_URL = "${BuildConfig.BACKEND_HOST}/backend/bot/standalone_worker.php"
private fun generateSignature(deviceId: String, ts: Long): String { private fun generateSignature(deviceId: String, ts: Long): String {
val message = "$deviceId$ts" val message = "$deviceId$ts"
+8
View File
@@ -15,6 +15,13 @@ android {
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 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 { buildTypes {
@@ -35,6 +42,7 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
buildConfig = true
} }
} }
@@ -1,38 +1,58 @@
package com.siro.socialmedia_bot.network package com.siro.socialmedia_bot.network
import com.siro.socialmedia_bot.BuildConfig
import org.json.JSONObject import org.json.JSONObject
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.net.URLEncoder
import java.util.Scanner import java.util.Scanner
/** /**
* Client to connect to the social_worker.php endpoint * عميل الباك إند للبوت الاجتماعي.
*
* ⚠️ نقطتا نهاية لا واحدة — وخلط الاثنتين كان سبب تعطّل التعليق العضوي:
*
* - [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 { object SocialBotClient {
private const val BASE_URL = "https://jordan-siro.intaleqapp.com/backend/marketing_engine/index.php" // `val` لا `const val`: قيم BuildConfig ليست ثوابت وقت ترجمة بنظر كوتلن،
private const val BOT_TOKEN = "YOUR_SECRET_BOT_TOKEN" // و`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? { fun getTask(platform: String): JSONObject? {
try { try {
val urlString = "$BASE_URL?action=get_task&platform=$platform" val urlString = "$ENGINE_URL?action=get_task&platform=${enc(platform)}"
android.util.Log.d("SocialBotClient", "Requesting URL: $urlString") android.util.Log.d("SocialBotClient", "Requesting URL: $urlString")
val url = URL(urlString) val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET" connection.requestMethod = "GET"
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN) connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
val responseCode = connection.responseCode val responseCode = connection.responseCode
android.util.Log.d("SocialBotClient", "Response Code: $responseCode") android.util.Log.d("SocialBotClient", "Response Code: $responseCode")
if (responseCode == 200) { if (responseCode == 200) {
val scanner = Scanner(connection.inputStream) val scanner = Scanner(connection.inputStream)
val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else "" val response = if (scanner.hasNext()) scanner.useDelimiter("\\A").next() else ""
scanner.close() scanner.close()
android.util.Log.d("SocialBotClient", "Response Body: $response") android.util.Log.d("SocialBotClient", "Response Body: $response")
val json = JSONObject(response) val json = JSONObject(response)
if (json.getString("status") == "success" && !json.isNull("data")) { if (json.getString("status") == "success" && !json.isNull("data")) {
return json.getJSONObject("data") return json.getJSONObject("data")
@@ -51,12 +71,17 @@ object SocialBotClient {
} }
fun completeTask(taskId: Int, result: String) { fun completeTask(taskId: Int, result: String) {
postData("action=complete_task", "task_id=$taskId&result=$result") 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? { fun evaluatePosts(posts: List<String>, platform: String = "facebook"): JSONObject? {
try { try {
val urlString = "$BASE_URL?action=evaluate_posts&platform=$platform" val urlString = "$ENGINE_URL?action=evaluate_posts&platform=${enc(platform)}"
val url = URL(urlString) val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST" connection.requestMethod = "POST"
@@ -80,9 +105,12 @@ object SocialBotClient {
scanner.close() scanner.close()
val json = JSONObject(response) val json = JSONObject(response)
if (json.getString("status") == "success" && !json.isNull("data")) { if (json.getString("status") == "success") {
return json.getJSONObject("data") 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) { } catch (e: Exception) {
android.util.Log.e("SocialBotClient", "Exception in evaluatePosts: ${e.message}") android.util.Log.e("SocialBotClient", "Exception in evaluatePosts: ${e.message}")
@@ -91,17 +119,20 @@ object SocialBotClient {
return null return null
} }
/** يخاطب [WORKER_URL] — `index.php` لا يعرف هذا الأمر إطلاقاً. */
fun processOrganicPost(deviceId: String, platform: String, postText: String, targetUrl: String): JSONObject? { fun processOrganicPost(deviceId: String, platform: String, postText: String, targetUrl: String): JSONObject? {
try { try {
val urlString = "$BASE_URL?action=process_organic_post" val urlString = "$WORKER_URL?action=process_organic_post"
val url = URL(urlString) val url = URL(urlString)
val connection = url.openConnection() as HttpURLConnection val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST" connection.requestMethod = "POST"
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN) connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
val params = "device_id=$deviceId&platform=$platform&post_text=${java.net.URLEncoder.encode(postText, "UTF-8")}&target_url=${java.net.URLEncoder.encode(targetUrl, "UTF-8")}"
val params = "device_id=${enc(deviceId)}&platform=${enc(platform)}" +
"&post_text=${enc(postText)}&target_url=${enc(targetUrl)}"
connection.doOutput = true connection.doOutput = true
val writer = OutputStreamWriter(connection.outputStream) val writer = OutputStreamWriter(connection.outputStream)
writer.write(params) writer.write(params)
writer.flush() writer.flush()
@@ -117,6 +148,9 @@ object SocialBotClient {
if (json.getString("status") == "success" && !json.isNull("data")) { if (json.getString("status") == "success" && !json.isNull("data")) {
return json.getJSONObject("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) { } catch (e: Exception) {
android.util.Log.e("SocialBotClient", "Exception in processOrganicPost: ${e.message}") android.util.Log.e("SocialBotClient", "Exception in processOrganicPost: ${e.message}")
@@ -126,29 +160,33 @@ object SocialBotClient {
} }
fun failTask(taskId: Int, errorMessage: String) { fun failTask(taskId: Int, errorMessage: String) {
postData("action=fail_task", "task_id=$taskId&error_message=$errorMessage") 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) { fun logMessage(accountId: Int?, level: String, message: String) {
val accIdParam = accountId?.let { "&account_id=$it" } ?: "" val accIdParam = accountId?.let { "&account_id=$it" } ?: ""
postData("action=log", "level=$level&message=$message$accIdParam") postData(WORKER_URL, "action=log", "level=${enc(level)}&message=${enc(message)}$accIdParam")
} }
private fun postData(actionParam: String, postBody: String) { private fun postData(endpoint: String, actionParam: String, postBody: String) {
try { try {
val url = URL("$BASE_URL?$actionParam") val url = URL("$endpoint?$actionParam")
val connection = url.openConnection() as HttpURLConnection val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST" connection.requestMethod = "POST"
connection.setRequestProperty("X-Bot-Token", BOT_TOKEN) connection.setRequestProperty("X-Bot-Token", BOT_TOKEN)
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded") connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
connection.doOutput = true connection.doOutput = true
val writer = OutputStreamWriter(connection.outputStream) val writer = OutputStreamWriter(connection.outputStream)
writer.write(postBody) writer.write(postBody)
writer.flush() writer.flush()
writer.close() writer.close()
connection.responseCode // execute val responseCode = connection.responseCode
if (responseCode != 200) {
android.util.Log.e("SocialBotClient", "$actionParam HTTP $responseCode")
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }