feat: implement FastAPI service with device-locked automation for Shopee commissions and Facebook commenting
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# Bot Affiliate & Auto Comment - N8N Integration API
|
||||
|
||||
Đây là máy chủ API viết bằng Python (FastAPI + UIAutomator2) dùng để điều khiển điện thoại Android tự động thực hiện các thao tác:
|
||||
1. **Shopee Affiliate**: Nhận link sản phẩm, tự động mở Shopee, chuyển đổi link lấy hoa hồng và trả về n8n.
|
||||
2. **Facebook Auto Comment**: Mở Group Facebook, tự động tìm bài viết, comment nội dung kèm hình ảnh động từ n8n.
|
||||
|
||||
## 1. Cài đặt và Khởi động
|
||||
Dự án yêu cầu cài đặt môi trường ảo (venv) và các thư viện trong `requirements.txt`.
|
||||
|
||||
**Khởi động Server:**
|
||||
Máy chủ đã được cài đặt như một Systemd Service (chạy ngầm tự động).
|
||||
- Bắt đầu service: `systemctl start shopeebot`
|
||||
- Xem trạng thái: `systemctl status shopeebot`
|
||||
- Khởi động lại khi sửa code: `systemctl restart shopeebot`
|
||||
- Xem log hoạt động: `journalctl -u shopeebot -f` hoặc xem file `shopee_bot.log`
|
||||
|
||||
---
|
||||
|
||||
## 2. Các API Endpoints cho N8N
|
||||
|
||||
### 2.1. API Lấy link Shopee Affiliate
|
||||
- **URL**: `POST /get_commission`
|
||||
- **Body (JSON)**:
|
||||
```json
|
||||
{
|
||||
"link": "https://s.shopee.vn/...",
|
||||
"device_ip": "10.160.138.2:5555"
|
||||
}
|
||||
```
|
||||
*(Lưu ý: `device_ip` là tùy chọn, nếu bỏ trống Bot sẽ tự lấy IP mặc định)*
|
||||
|
||||
- **Kết quả trả về**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"commission": "10.5%",
|
||||
"affiliate_link": "https://s.shopee.vn/xyz...",
|
||||
"price": "đ90.000",
|
||||
"message": "Trích xuất thành công"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2. API Tự động Comment Facebook
|
||||
- **URL**: `POST /post_facebook_comment`
|
||||
- **Body (JSON)**:
|
||||
```json
|
||||
{
|
||||
"device_ip": "10.160.138.2:5555",
|
||||
"group_id": "123456789",
|
||||
"content": "Nội dung comment mà bạn muốn gõ...",
|
||||
"image_url": "https://example.com/anh.jpg"
|
||||
}
|
||||
```
|
||||
*(Lưu ý: `image_url` là tùy chọn, nếu bài nào không cần ảnh thì không cần gửi trường này).*
|
||||
|
||||
- **Luồng hoạt động**:
|
||||
1. Python tải ảnh về và đẩy vào thư viện Android.
|
||||
2. Dùng Deep Link mở thẳng vào Group.
|
||||
3. Bấm "Bình luận" ở bài viết đầu tiên.
|
||||
4. Gõ Text, chọn ảnh vừa tải và bấm "Gửi".
|
||||
5. Xóa ảnh tạm để dọn dẹp bộ nhớ.
|
||||
|
||||
---
|
||||
|
||||
## 3. Quản lý Màn Hình Điện Thoại (Chống chạm nhầm)
|
||||
|
||||
Để điện thoại chạy Auto mượt mà 24/7 mà không bị ai chạm nhầm vào màn hình và không tốn pin, hãy dùng 2 script có sẵn:
|
||||
|
||||
1. **Tắt đen màn hình & Khóa cảm ứng**:
|
||||
Chạy lệnh: `./disable_screen.sh`
|
||||
*(Màn hình vật lý sẽ đen thui, không thể bấm, nhưng Bot vẫn chạy ngầm bên trong)*
|
||||
|
||||
2. **Bật lại màn hình bình thường**:
|
||||
Chạy lệnh: `./enable_screen.sh`
|
||||
|
||||
3. **Chống điện thoại tự khóa (Sleep/Lock screen)**:
|
||||
Chạy lệnh ADB sau để ép điện thoại thức vĩnh viễn:
|
||||
```bash
|
||||
adb -s 10.160.138.2:5555 shell svc power stayon true
|
||||
```
|
||||
*(Muốn tắt chức năng này thì đổi chữ `true` thành `false`).*
|
||||
|
||||
|
||||
|
||||
💡 Mẹo thiết lập trên n8n: Thay vì phải nhớ từng ID, bạn nên lập một file Google Sheet.
|
||||
Google Sheet: https://docs.google.com/spreadsheets/d/1Oje_uRqSd3pKf0hrE5M10Aqq9gEp-qHvGmtIkrq86fU/edit?usp=sharing
|
||||
(Tên Group Group ID Content Link hình đính kèm)
|
||||
|
||||
|
||||
Cột A: Tên Group (Để bạn tự nhớ).
|
||||
Cột B: Group ID (Dãy số).
|
||||
Cột C: Link bài viết (Nếu cần lưu). Sau đó cho n8n đọc Google Sheet này, lấy giá trị ở Cột B rồi bắn sang cho Python Bot. Chạy vòng lặp (Loop) là Bot sẽ tự động nhảy hết từ Group này sang Group khác để rải Comment!
|
||||
|
||||
|
||||
|
||||
Client ID: 745351236177-ibrcl3j2d9oi2malj3fpatfu6il2290b.apps.googleusercontent.com
|
||||
Client secret: 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
|
||||
( C:\Users\NASPC\Documents\botAffilate\facebook\client_secret_745351236177-ibrcl3j2d9oi2malj3fpatfu6il2290b.apps.googleusercontent.com.json )
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import logging
|
||||
import uiautomator2 as u2
|
||||
import time
|
||||
import os
|
||||
|
||||
class FacebookBot:
|
||||
def __init__(self, device_ip: str):
|
||||
logging.info(f"Đang kết nối tới thiết bị {device_ip} để chạy Facebook...")
|
||||
self.d = u2.connect(device_ip)
|
||||
logging.info("Kết nối thành công!")
|
||||
|
||||
def push_image_to_gallery(self, local_image_path: str):
|
||||
if not local_image_path or not os.path.exists(local_image_path):
|
||||
logging.warning("Không có file ảnh cục bộ để đẩy lên điện thoại.")
|
||||
return False
|
||||
|
||||
logging.info(f"Đang đẩy ảnh {local_image_path} vào điện thoại...")
|
||||
# Đẩy ảnh vào thư mục Pictures
|
||||
remote_path = "/sdcard/Pictures/n8n_fb_comment.jpg"
|
||||
self.d.push(local_image_path, remote_path)
|
||||
|
||||
# Gọi Media Scanner để Thư viện ảnh nhận diện ngay lập tức
|
||||
logging.info("Đang cập nhật thư viện ảnh...")
|
||||
self.d.shell(f'am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file://{remote_path}')
|
||||
time.sleep(2)
|
||||
return True
|
||||
|
||||
def post_comment(self, group_id: str, content: str, local_image_path: str = None) -> dict:
|
||||
try:
|
||||
self.d.screen_on()
|
||||
|
||||
# Đẩy ảnh vào điện thoại nếu có
|
||||
has_image = False
|
||||
if local_image_path:
|
||||
has_image = self.push_image_to_gallery(local_image_path)
|
||||
|
||||
logging.info(f"Khởi động Facebook và vào Group ID: {group_id}...")
|
||||
# Dùng lệnh ADB shell để mở thẳng Deep Link của Group
|
||||
self.d.shell(f'am start -a android.intent.action.VIEW -d "fb://group/{group_id}"')
|
||||
time.sleep(8) # Đợi load giao diện group
|
||||
|
||||
# Tìm nút "Bình luận" của bài viết đầu tiên hiển thị
|
||||
logging.info("Tìm nút Bình luận của bài viết đầu tiên...")
|
||||
# Facebook dùng description hoặc text, thường là "Bình luận" hoặc "Comment"
|
||||
comment_btn = self.d(descriptionContains="Bình luận")
|
||||
if not comment_btn.exists:
|
||||
comment_btn = self.d(descriptionContains="Comment")
|
||||
|
||||
if comment_btn.exists:
|
||||
comment_btn[0].click() # Click vào bài đầu tiên tìm thấy
|
||||
else:
|
||||
self.dump_ui_for_debug("error_fb_no_comment_btn.xml")
|
||||
return {"status": "error", "message": "Không tìm thấy nút Bình luận. Đã lưu dump UI."}
|
||||
|
||||
time.sleep(4)
|
||||
|
||||
# Gõ nội dung
|
||||
logging.info("Đang nhập nội dung comment...")
|
||||
edit_box = self.d(className="android.widget.EditText")
|
||||
if edit_box.exists:
|
||||
edit_box.set_text(content)
|
||||
time.sleep(1)
|
||||
else:
|
||||
self.dump_ui_for_debug("error_fb_no_edittext.xml")
|
||||
return {"status": "error", "message": "Không tìm thấy ô nhập chữ."}
|
||||
|
||||
# Chọn ảnh nếu có
|
||||
if has_image:
|
||||
logging.info("Đang chọn ảnh đính kèm...")
|
||||
# Nút chọn ảnh thường có chữ Camera hoặc Ảnh/Video
|
||||
photo_btn = self.d(descriptionContains="ảnh")
|
||||
if not photo_btn.exists:
|
||||
photo_btn = self.d(descriptionContains="photo")
|
||||
|
||||
if photo_btn.exists:
|
||||
photo_btn.click()
|
||||
time.sleep(3)
|
||||
|
||||
# Chọn ảnh đầu tiên trong thư viện (vừa mới push vào)
|
||||
# Facebook gallery thường có checkbox hoặc chỉ cần click vào vùng ảnh
|
||||
gallery_images = self.d(className="android.widget.ImageView")
|
||||
if gallery_images.count >= 2:
|
||||
gallery_images[1].click() # Bỏ qua icon đầu tiên thường là icon camera
|
||||
time.sleep(1)
|
||||
|
||||
# Nút Xong / Done
|
||||
done_btn = self.d(textContains="Xong")
|
||||
if not done_btn.exists:
|
||||
done_btn = self.d(textContains="Done")
|
||||
if done_btn.exists:
|
||||
done_btn.click()
|
||||
time.sleep(1)
|
||||
else:
|
||||
logging.warning("Không tìm thấy nút đính kèm ảnh, sẽ chỉ gửi text.")
|
||||
|
||||
# Bấm gửi
|
||||
logging.info("Bấm nút Gửi (Send)...")
|
||||
send_btn = self.d(descriptionContains="Gửi")
|
||||
if not send_btn.exists:
|
||||
send_btn = self.d(descriptionContains="Send")
|
||||
|
||||
if send_btn.exists:
|
||||
send_btn.click()
|
||||
time.sleep(3)
|
||||
return {"status": "success", "message": "Đã comment thành công!"}
|
||||
else:
|
||||
self.dump_ui_for_debug("error_fb_no_send_btn.xml")
|
||||
return {"status": "error", "message": "Không tìm thấy nút Gửi."}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Lỗi Facebook Bot: {str(e)}")
|
||||
return {"status": "error", "message": f"Lỗi không xác định: {str(e)}"}
|
||||
|
||||
finally:
|
||||
logging.info("Thoát ra màn hình chính (Homescreen)...")
|
||||
self.d.press("home")
|
||||
|
||||
def dump_ui_for_debug(self, filename: str):
|
||||
xml_dump = self.d.dump_hierarchy()
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(xml_dump)
|
||||
logging.error(f"Đã lưu giao diện lỗi vào file {filename}")
|
||||
+57
-7
@@ -5,6 +5,14 @@ from typing import Optional
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import uuid
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
from facebook_bot import FacebookBot
|
||||
|
||||
# Quản lý hàng đợi (Lock) cho từng thiết bị
|
||||
device_locks = defaultdict(threading.Lock)
|
||||
|
||||
# Cấu hình logging: ghi log ra cả màn hình (console) và file (shopee_bot.log)
|
||||
logging.basicConfig(
|
||||
@@ -23,19 +31,26 @@ class CommissionRequest(BaseModel):
|
||||
# Cho phép truyền IP linh hoạt từ n8n.
|
||||
device_ip: Optional[str] = Field(default=None, description="IP:Port của điện thoại, ví dụ 192.168.1.100:5555 (WiFi) hoặc 10.8.0.5:5555 (VPN)")
|
||||
|
||||
class FacebookCommentRequest(BaseModel):
|
||||
device_ip: Optional[str] = Field(default=None, description="IP:Port của điện thoại (Cấu hình trên n8n)")
|
||||
group_id: str
|
||||
content: str
|
||||
image_url: Optional[str] = None
|
||||
|
||||
@app.post("/get_commission")
|
||||
async def get_commission_endpoint(req: CommissionRequest):
|
||||
def get_commission_endpoint(req: CommissionRequest):
|
||||
if not req.link:
|
||||
raise HTTPException(status_code=400, detail="Thiếu link sản phẩm")
|
||||
|
||||
try:
|
||||
# Lấy IP theo độ ưu tiên:
|
||||
# 1. Truyền từ Request Body của n8n
|
||||
# 2. Truyền từ biến môi trường DEVICE_IP của Armbian/Windows
|
||||
# 3. Để rỗng "" (Thư viện u2 sẽ tự tìm thiết bị ADB đang cắm/connect)
|
||||
target_ip = req.device_ip or os.environ.get("DEVICE_IP") or ""
|
||||
# Lấy IP theo độ ưu tiên
|
||||
raw_ip = req.device_ip or os.environ.get("DEVICE_IP") or ""
|
||||
lock_key = raw_ip if raw_ip else "default_device"
|
||||
|
||||
bot = ShopeeBot(device_ip=target_ip)
|
||||
# Dùng Lock để xếp hàng: Nếu điện thoại đang bận, request sau phải chờ
|
||||
with device_locks[lock_key]:
|
||||
logging.info(f"Đã khóa thiết bị {lock_key} để chạy tác vụ Shopee.")
|
||||
bot = ShopeeBot(device_ip=raw_ip)
|
||||
|
||||
result = bot.get_commission_rate(req.link)
|
||||
|
||||
@@ -51,6 +66,41 @@ async def get_commission_endpoint(req: CommissionRequest):
|
||||
logging.error(f"Lỗi API: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/post_facebook_comment")
|
||||
def post_facebook_comment(req: FacebookCommentRequest):
|
||||
try:
|
||||
raw_ip = req.device_ip or os.environ.get("DEVICE_IP") or ""
|
||||
lock_key = raw_ip if raw_ip else "default_device"
|
||||
|
||||
with device_locks[lock_key]:
|
||||
logging.info(f"Đã khóa thiết bị {lock_key} để chạy tác vụ Facebook.")
|
||||
bot = FacebookBot(device_ip=raw_ip)
|
||||
|
||||
local_image_path = None
|
||||
if req.image_url:
|
||||
# Tải ảnh về máy chủ Python tạm thời
|
||||
local_image_path = f"temp_fb_{uuid.uuid4().hex}.jpg"
|
||||
urllib.request.urlretrieve(req.image_url, local_image_path)
|
||||
logging.info(f"Đã tải ảnh tạm thời: {local_image_path}")
|
||||
|
||||
result = bot.post_comment(req.group_id, req.content, local_image_path)
|
||||
|
||||
# Dọn dẹp ảnh tạm sau khi xong
|
||||
if local_image_path and os.path.exists(local_image_path):
|
||||
os.remove(local_image_path)
|
||||
logging.info(f"Đã xóa ảnh tạm: {local_image_path}")
|
||||
|
||||
if isinstance(result, dict) and "status" in result:
|
||||
if result["status"] == "error":
|
||||
raise HTTPException(status_code=500, detail=result.get("message"))
|
||||
return result
|
||||
|
||||
return {"status": "success", "message": "Hoàn tất"}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Lỗi API Facebook: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
# Chạy server API ở cổng 8000
|
||||
|
||||
Reference in New Issue
Block a user