226 lines
12 KiB
Python
226 lines
12 KiB
Python
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
|
|
|
|
import uuid
|
|
logging.info(f"Đang đẩy ảnh {local_image_path} vào điện thoại...")
|
|
# Tạo tên file độc nhất để đảm bảo luôn hiện lên ĐẦU thư viện ảnh
|
|
random_name = f"n8n_fb_{uuid.uuid4().hex[:8]}.jpg"
|
|
remote_path = f"/sdcard/Pictures/{random_name}"
|
|
|
|
# Xóa bớt các file tạm cũ trước khi đẩy file mới (giữ điện thoại sạch sẽ)
|
|
self.d.shell("rm -f /sdcard/Pictures/n8n_fb_*.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, post_index: int = 1) -> 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 URL của Group (Ép mở bằng app Facebook)
|
|
# Dùng https:// thay vì fb:// vì một số Group bị lỗi deep link "Không hiển thị"
|
|
self.d.shell(f'am start -a android.intent.action.VIEW -d "https://www.facebook.com/groups/{group_id}" com.facebook.katana')
|
|
time.sleep(8) # Đợi load giao diện group
|
|
self.dump_ui_for_debug("fb_step1_open_group.xml")
|
|
|
|
# Kiểm tra xem Group có bị lỗi "Không hiển thị" (Do sai ID, group chết, group kín) hay không
|
|
err_msg1 = self.d(descriptionContains="Nội dung này không hiển thị")
|
|
err_msg2 = self.d(descriptionContains="chỉ hiển thị với một số người")
|
|
if err_msg1.exists or err_msg2.exists:
|
|
logging.warning(f"Group {group_id} không tồn tại hoặc bạn không có quyền truy cập.")
|
|
return {"status": "error", "message": "Group không tồn tại hoặc không có quyền truy cập."}
|
|
|
|
# Quét nút Bình luận, nếu không thấy thì vuốt xuống (vì ảnh bìa group che mất)
|
|
logging.info(f"Tìm nút Bình luận của bài viết thứ {post_index}...")
|
|
comment_btn = None
|
|
current_post_count = 1
|
|
|
|
for _ in range(30): # Tăng số lần vuốt tối đa lên để có thể cuộn qua nhiều bài
|
|
# Sửa lỗi: Nút Thích có description là "bày tỏ cảm xúc về bình luận" nên Regex .*bình luận.* sẽ bắt nhầm nút Thích!
|
|
# Do đó ta chỉ tìm đúng chữ Bình luận đứng đầu, hoặc chữ Comment.
|
|
c_desc_vn = self.d(descriptionMatches="(?i)^bình luận$")
|
|
c_text_vn = self.d(textMatches="(?i)^bình luận$")
|
|
c_desc_en = self.d(descriptionMatches="(?i)^comment$")
|
|
c_text_en = self.d(textMatches="(?i)^comment$")
|
|
|
|
# Lấy tất cả các nút có thể là nút Bình luận
|
|
candidates = []
|
|
if c_desc_vn.exists: candidates.extend([c_desc_vn[i] for i in range(c_desc_vn.count)])
|
|
if c_text_vn.exists: candidates.extend([c_text_vn[i] for i in range(c_text_vn.count)])
|
|
if c_desc_en.exists: candidates.extend([c_desc_en[i] for i in range(c_desc_en.count)])
|
|
if c_text_en.exists: candidates.extend([c_text_en[i] for i in range(c_text_en.count)])
|
|
|
|
# Chỉ lấy những nút nằm dưới khu vực Header của Facebook (Y > 350)
|
|
btn = None
|
|
for c in candidates:
|
|
if c.info["bounds"]["bottom"] > 350:
|
|
btn = c
|
|
break
|
|
|
|
if btn:
|
|
if current_post_count == post_index:
|
|
comment_btn = btn
|
|
break
|
|
else:
|
|
logging.info(f"Đã thấy bài viết thứ {current_post_count}, đang đẩy nó vào góc khuất để tìm bài {post_index}...")
|
|
bounds = btn.info["bounds"]
|
|
start_y = bounds["bottom"]
|
|
|
|
# Tính tổng quãng đường cần vuốt để đẩy nút này lên tọa độ Y=200
|
|
distance_needed = start_y - 200
|
|
|
|
if distance_needed > 0:
|
|
screen_width = self.d.info['displayWidth']
|
|
mid_x = screen_width // 2
|
|
|
|
# Cắt nhỏ quãng đường vuốt (tối đa 1000px mỗi cú) và vuốt từ GIỮA màn hình
|
|
# để tránh vuốt nhầm vào thanh điều hướng ở mép dưới hoặc mép trên.
|
|
while distance_needed > 0:
|
|
swipe_dist = min(distance_needed, 1000)
|
|
self.d.swipe(mid_x, 1500, mid_x, 1500 - swipe_dist, duration=0.3)
|
|
time.sleep(1)
|
|
distance_needed -= swipe_dist
|
|
|
|
time.sleep(1) # Nghỉ một nhịp sau khi vuốt xong
|
|
|
|
current_post_count += 1
|
|
continue
|
|
|
|
# Nếu không thấy nút nào, vuốt lên một chút để tìm tiếp
|
|
self.d.swipe_ext("up", scale=0.5)
|
|
time.sleep(1.5)
|
|
self.dump_ui_for_debug("fb_step2_scrolling.xml")
|
|
|
|
if comment_btn and comment_btn.exists:
|
|
comment_btn[0].click() # Click vào bài đầu tiên tìm thấy
|
|
self.dump_ui_for_debug("fb_step3_clicked_comment.xml")
|
|
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)
|
|
|
|
# Tìm ô nhập liệu "Viết bình luận công khai..."
|
|
logging.info("Nhập nội dung text...")
|
|
# Facebook có thể dùng EditText hoặc một class khác tùy phiên bản
|
|
edit_box = self.d(className="android.widget.EditText")
|
|
if not edit_box.exists:
|
|
# Fallback: Tìm ô chứa chữ "Viết bình luận" hoặc "Write a comment"
|
|
edit_box = self.d(descriptionMatches="(?i).*viết bình luận.*")
|
|
if not edit_box.exists:
|
|
edit_box = self.d(textMatches="(?i).*viết bình luận.*")
|
|
|
|
if edit_box.exists:
|
|
edit_box.click()
|
|
time.sleep(1)
|
|
edit_box.set_text(content)
|
|
self.dump_ui_for_debug("fb_step7_text_entered.xml")
|
|
else:
|
|
self.dump_ui_for_debug("error_fb_no_edit_box.xml")
|
|
return {"status": "error", "message": "Không tìm thấy ô gõ chữ. Có thể bạn bị cấm comment ở Group này."}
|
|
|
|
# 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)
|
|
self.dump_ui_for_debug("fb_step5_camera_opened.xml")
|
|
|
|
# Facebook gallery mới dùng Button với description 'Ảnh, mục X...'
|
|
gallery_images = self.d(descriptionContains="Ảnh, mục")
|
|
if not gallery_images.exists:
|
|
gallery_images = self.d(descriptionContains="Photo")
|
|
if not gallery_images.exists:
|
|
gallery_images = self.d(descriptionContains="Ảnh")
|
|
|
|
if gallery_images.exists:
|
|
# Bấm vào ảnh đầu tiên (ảnh vừa mới push vào điện thoại sẽ nằm ở đầu danh sách)
|
|
gallery_images[0].click()
|
|
time.sleep(1)
|
|
self.dump_ui_for_debug("fb_step6_image_selected.xml")
|
|
|
|
# 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:
|
|
# Nếu không có nút Xong, bấm Back để gập cái thư viện ảnh lại, vì nó đang che khuất nút Gửi!
|
|
logging.info("Không có nút Xong, gập thư viện ảnh xuống...")
|
|
self.d.press("back")
|
|
time.sleep(1.5)
|
|
|
|
# Tìm nút "Gửi" (Send)
|
|
logging.info("Tìm nút Gửi...")
|
|
send_btn = self.d(descriptionMatches="(?i)^gửi$")
|
|
if not send_btn.exists:
|
|
send_btn = self.d(descriptionMatches="(?i)^send$")
|
|
if not send_btn.exists:
|
|
send_btn = self.d(descriptionContains="Gửi")
|
|
|
|
if send_btn.exists:
|
|
send_btn.click()
|
|
time.sleep(3)
|
|
|
|
# Dọn dẹp bộ nhớ cho điện thoại: xóa file ảnh ngay lập tức sau khi post xong
|
|
if has_image:
|
|
logging.info("Đang dọn dẹp file ảnh tạm trên điện thoại...")
|
|
self.d.shell("rm -f /sdcard/Pictures/n8n_fb_*.jpg")
|
|
# Cập nhật lại thư viện ảnh để nó xóa hẳn khỏi Gallery
|
|
self.d.shell('am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/Pictures/')
|
|
|
|
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="ui_dump.xml"):
|
|
import os
|
|
os.makedirs("xml_ui", exist_ok=True)
|
|
filepath = os.path.join("xml_ui", filename)
|
|
xml_content = self.d.dump_hierarchy()
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.write(xml_content)
|
|
logging.info(f"Đã lưu cấu trúc UI hiện tại ra file {filepath} để debug.")
|