Files
autman-plugins/py/jd_rabbit_账密.py
2026-05-15 21:26:06 +08:00

646 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# [author: authook]
# [title: jd_账密登陆]
# [create_at: 2023-09-19 15:29:17]
# [version: 2.3]
# [pin: true]
# [price: 0]
# [rule: ^账密登录$]
# [rule: ^账密登陆$]
# [rule: ^账密刷新$]
# [rule: ^账密强制刷新$]
# [rule: ^账密管理$]
# [priority: 6666666]优先级
# [admin: false]
# [service: authook]
# [icon: https://bbs.autman.cn/assets/files/2024-05-16/1715842374-826456-2584e965be6ed12773865dbb8e29a449.png]
# [description: jd_账密登陆插件 兼容兔子,pro等框架</br>指令:账密登录,账密登录,账密管理 </br> 配置:在插件配参中配置,</br>权限:需开启qlsjdNotify权限</br>注意⚠️账密刷新请勿手动发送,否则会将ck绑定至管理员账号上!!!(插件已自动内置定时刷新账密)]
# [param: {"required":true,"key":"jd.rabbit_host","placeholder":"http://192.168.1.1:81","name":"【rabbit】登陆地址:","desc":"登陆地址,例:http://192.168.3.22"}]
# [param: {"required":true,"key":"jd.pro_host","placeholder":"http://192.168.1.1:81","name":"【pro】登陆地址:","desc":"pro登陆地址"}]
# [param: {"required":true,"key":"jd.login_ql_name","placeholder":"ql容器名称","name":"【ql】容器名称","desc":"ql容器名称"}]
# [param: {"required":true,"key":"jd.login_type","placeholder":"登陆框架","name":"登陆框架","desc":"pro框架请输入pro rabbit兔子框架请输入rabbit"}]
# =======================
# 项目:jd_账密登陆.py
# 构建时间:2025-02-10 20:52:29
import datetime
from email import message
import json
import logging
import math
import os
import random
import re
import time
import traceback
import inspect
from base64 import b64encode
from urllib.parse import quote, urlparse
import colorlog
import middleware
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
class TOOL:
def __init__(self):
self.plugin_pre = "rabbit_zm"
self.remark = ""
self.platform_arr = self.platformArr()
# 将日期和时间格式化为字符串
self.cut_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.logger = logging.getLogger()
self.color_formatter = colorlog.ColoredFormatter(
'%(log_color)s%(levelname)s: %(message)s',
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
}
)
self.logger = self._configure_logger()
def _configure_logger(self):
"""配置并返回一个带颜色的 logger 对象"""
logger = logging.getLogger(self.plugin_pre)
logger.setLevel(logging.DEBUG) # 设置日志默认最低级别
# 检查是否已添加过控制台处理器,避免重复添加
if not any(isinstance(handler, logging.StreamHandler) for handler in logger.handlers):
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG) # 控制台日志最低级别
console_handler.setFormatter(self.color_formatter)
logger.addHandler(console_handler)
return logger
def log(self, level, message):
"""通用日志方法"""
if level == logging.INFO:
self.logger.info(message)
elif level == logging.ERROR:
self.logger.error(message)
elif level == logging.WARNING:
self.logger.warning(message)
elif level == logging.DEBUG:
self.logger.debug(message)
elif level == logging.CRITICAL:
self.logger.critical(message)
def log_info(self, message):
self.log(logging.INFO, message)
def log_err(self, message):
self.log(logging.ERROR, message)
def log_warn(self, message):
self.log(logging.WARNING, message)
def platformArr(self):
plat_arr = []
for p in ["qq", "wx", "wb", "qb", "bt", "tg"]:
t = {"platform": f"{p}", "imType": p}
plat_arr.append(t)
return plat_arr
def pushMsg(self, userId, chatId, imType, title, content):
# 获取当前日期和时间
now = datetime.datetime.now()
# 将日期和时间格式化为字符串
formatted_datetime = now.strftime('%Y-%m-%d %H:%M:%S')
self.remark = ""
return middleware.push(imType, chatId, userId, title, f"{formatted_datetime}\n{content}")
def pushGroup(self, imtype, groupCode, content):
self.pushMsg(0, groupCode, imtype, "", content)
def autman_host(self):
file_path = inspect.getframeinfo(inspect.currentframe()).filename
dir_path = os.path.dirname(os.path.abspath(file_path))
setConfPath = dir_path.split("develop/scripts")[0] + "/middleware.py"
try:
os.setuid(0) # 强制转为root 前提是autman账户以root用户启用
with open(setConfPath, "r", encoding="utf-8") as f:
data = f.read()
lines = data.splitlines()
for line in lines:
if "url =" in line:
res = line.split('"')
url = res[1][0:-5]
self.log_info(url)
return url
except Exception as e:
self.log_info(f"❌autman_host error {e}")
def replyMsg(self, content):
return sender.reply(content)
def pushMaster(self, content):
arr = ["qq", "wx", "tg", "qb", 'wb']
for p in arr:
masters = sender.bucketGet(p, "masters")
if masters:
masters_arr = masters.split(",") if "," in masters else masters.split("&")
for m in masters_arr:
middleware.push(p, "", m, "", content)
def qls(self):
qls = []
ql_id_arr = sender.bucketAllKeys("qls")
if not ql_id_arr:
return []
for ql_id in ql_id_arr:
value = sender.bucketGet("qls", ql_id)
if value:
qls.append(json.loads(value))
return qls
def autman_host(self):
file_path = inspect.getframeinfo(inspect.currentframe()).filename
dir_path = os.path.dirname(os.path.abspath(file_path))
setConfPath = dir_path.split("develop/scripts")[0] + "/middleware.py"
try:
os.setuid(0) # 强制转为root 前提是autman账户以root用户启用
with open(setConfPath, "r", encoding="utf-8") as f:
data = f.read()
lines = data.splitlines()
for line in lines:
if "url =" in line:
res = line.split('"')
url = res[1][0:-5]
self.log_info(url)
return url
except Exception as e:
self.log_info(f"❌autman_host error {e}")
class QL:
def __init__(self, ql):
self.host = ql["host"]
self.client_id = ql["client_id"]
self.client_secret = ql["client_secret"]
self.token = self.get_token()
self.name = ql['name']
self.content = ""
self.version = self.system()
def gettimestamp(self):
return str(int(time.time() * 1000))
def get_token(self):
url = f"{self.host}/open/auth/token?client_id={self.client_id}&client_secret={self.client_secret}"
print(url)
headers = {
'Content-Type': 'application/json',
'Accept': '*/*',
'Cache-Control': 'no-cache',
'Host': "",
'Connection': 'keep-alive'
}
host = urlparse(url).netloc
if ":" in host:
host = host.split(":")[0]
headers['Host'] = host
try:
response = requests.get(url, timeout=10, headers=headers)
res = response.json()
if response.status_code == 200 and res["code"] == 200:
return res["data"]["token"]
else:
return None
except Exception as e:
return None
def Api(self, api, apd, method, payload):
url = f"{self.host}/open/{api}/{apd}"
headers = {
"Content-Type": "application/json;charset=UTF-8",
"Authorization": f"Bearer {self.token}",
}
try:
response = requests.request(method, url, headers=headers, data=payload, timeout=5)
return response.json()
except Exception as e:
return None
# 环境变量
def envGet(self, keyword):
return self.Api('envs', f"?searchValue={keyword}&t={self.gettimestamp()}", 'GET', {})
def envSet(self, env):
return self.Api('envs', f"?t={self.gettimestamp()}", 'POST', env)
def envUpdate(self, env):
return self.Api('envs', f"?t={self.gettimestamp()}", 'PUT', env)
def envEnable(self, ids):
return self.Api('envs', f"/enable?t={self.gettimestamp()}", 'PUT', json.dumps(ids))
def envDisable(self, ids):
return self.Api('envs', f"/disable?t={self.gettimestamp()}", 'PUT', json.dumps(ids))
def envDel(self, ids):
return self.Api('envs', f"?t={self.gettimestamp()}", 'delete', json.dumps(ids))
def envMove(self, id, data):
return self.Api('envs', f"/{id}/move?t={self.gettimestamp()}", 'PUT', json.dumps(data))
# 定时任务
def cronGet(self, keyword):
return self.Api('crons', f"?searchValue={keyword}&t={self.gettimestamp()}", 'GET', None)
def cronRun(self, ids):
return self.Api('crons', f"run?t={self.gettimestamp()}", 'PUT', ids)
def cronStop(self, ids):
return self.Api('crons', f"stop?t={self.gettimestamp()}", 'PUT', ids)
def cronEnable(self, ids):
return self.Api('crons', f"enable?t={self.gettimestamp()}", 'PUT', ids)
def cronDisable(self, ids):
return self.Api('crons', f"disable?t={self.gettimestamp()}", 'PUT', ids)
def cronDetail(self, cronId):
return self.Api('crons', f"{cronId}?t={self.gettimestamp()}", 'GET', None)
def cronLogs(self, cronId):
return self.Api('crons', f"{cronId}/log?t={self.gettimestamp()}", 'GET', None)
def cronLog(self, cronId):
return self.Api('crons', f"{cronId}/log?t={self.gettimestamp()}", 'GET', None)
# 配置文件
def configsGet(self, name):
if self.version < "2.17":
return self.Api('configs', f"{name}?t={self.gettimestamp()}", 'GET', None)
else:
return self.Api('configs/detail', f"?path={name}&t={self.gettimestamp()}", 'GET', None)
def configsSet(self, data):
return self.Api('configs', f"save?t={self.gettimestamp()}", 'POST', data)
# 订阅任务
def subsGet(self, keyword):
return self.Api('subscriptions', f"?searchValue={keyword}&t={self.gettimestamp()}", 'GET', None)
def subsSet(self, data):
return self.Api('subscriptions', f"save?t={self.gettimestamp()}", 'PUT', data)
def subsRun(self, ids):
return self.Api('subscriptions', f"run?t={self.gettimestamp()}", 'PUT', ids)
def subsEnable(self, ids):
return self.Api('subscriptions', f"enable?t={self.gettimestamp()}", 'PUT', ids)
def subsDisable(self, ids):
return self.Api('subscriptions', f"disable?t={self.gettimestamp()}", 'PUT', ids)
def subsLog(self, subId):
return self.Api('subscriptions', f"{subId}/log?t={self.gettimestamp()}", 'GET', None)
def system(self):
url = f"{self.host}/open/system?t={self.gettimestamp()}"
headers = {
"Content-Type": "application/json;charset=UTF-8",
}
try:
res = requests.get(url)
if res.status_code == 200:
rj = res.json()
return rj['data']['version']
except Exception as e:
return 2.16
class ZM_RABBIT():
def __init__(self):
self.pins = self.get_pins()
self.conf = self.get_conf()
self.init()
self.s = requests.session()
pass
def hide_phone(self, text):
if not text:
return text
if len(text) != 11:
return text
return re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', text)
def check_ck_status(self, ck):
headers = {
"Cookie": ck,
"referer": "https://h5.m.jd.com/",
"User-Agent": "jdapp;iPhone;10.1.2;15.0;network/wifi;Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148;supportJDSHWK/1",
}
response = requests.get('https://plogin.m.jd.com/cgi-bin/ml/islogin', headers=headers)
data = response.json()
return data['islogin'] == "1" if 'islogin' in data else False
def en_pwd(self, pwd):
key = b"wn:_/imCQF;rBNS{V=|kML[(+$%Tjgy)"
iv = b"zA#f.x>jN!h-0[{$"
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(pwd.encode(), AES.block_size))
return b64encode(encrypted).decode()
def get_conf(self):
self.conf = {
"paid": "n",
"fee": 0,
"qr_code": "",
"host": "",
"ql_name": None
}
if middleware.bucketGet("otto", "rabbit_pro_paid"):
self.conf["paid"] = middleware.bucketGet("otto", "rabbit_pro_paid")
if middleware.bucketGet("otto", "rabbit_pro_fee"):
self.conf['fee'] = middleware.bucketGet("otto", "rabbit_pro_fee")
if middleware.bucketGet("otto", "rabbit_pro_qr_code"):
self.conf['qr_code'] = middleware.bucketGet("otto", "rabbit_pro_qr_code")
if not middleware.bucketGet("otto", "rabbit_pro_host"):
tool.pushMaster("未配置rabbit登录地址,请先去【配参】配置")
exit()
self.conf['host'] = middleware.bucketGet("otto", "rabbit_pro_host")
if not middleware.bucketGet("otto", "rabbit_ql_name"):
tool.pushMaster("未配置rabbit登陆容器名称,请先去【配参】配置")
exit()
self.conf['ql_name'] = middleware.bucketGet("otto", "rabbit_ql_name")
return self.conf
def init(self):
res = requests.get(self.conf['host'] + "/api/Config")
tool.log_info(res.text)
if res.status_code == 200:
rj = res.json()
if rj['success']:
self.conf['container_id'] = rj['data']['list'][0]['container_id']
self.conf['container_name'] = rj['data']['list'][0]['container_name']
self.conf['autocount'] = rj['data']['autocount']
return self.conf
tool.replyMsg(rj['message'] + ",请完善rabbitPro配置")
exit()
def get_pins(self):
return sender.bucketKeys("pin" + imType.upper(), userId)
def account_list(self):
acc_list = []
acc_key = set()
if self.pins and len(self.pins) > 0:
for pin in self.pins:
acc_str = sender.bucketGet("", pin)
if acc_str:
acc = {
"username": acc_str.split("#")[0],
"pwd": acc_str.split("#")[1],
"ck": acc_str.split("#")[2],
}
if acc_str.split("#")[0] not in acc_key:
acc_list.append(acc)
acc_key.add(acc_str.split("#")[0])
return acc_list
def login_init(self, mobile):
headers = {
"Content-Type": "application/json",
"User_Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
}
data = {"account": mobile, "container_id": self.conf['container_id']}
url = self.conf['host'] + "/pwd/init"
res = self.s.post(url, headers=headers, data=json.dumps(data))
tool.log_info(f"{mobile}: login_init res {res.text}")
if res.status_code == 200:
rj = res.json()
if not rj['success']:
tool.replyMsg(f"[{self.hide_phone(mobile)}]登陆初始化失败:{rj['message']}")
return
if rj['data']['status'] == 505:
return True
else:
tool.replyMsg(f"f{rj['message']}")
return False
pass
def login(self, mobile, pwd):
headers = {
"Content-Type": "application/json",
"User_Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
}
data = {"account": mobile, "pwd": self.en_pwd(pwd), "container_id": self.conf['container_id']}
res = self.s.post(f"{self.conf['host']}/pwd/login", headers=headers, data=json.dumps(data))
tool.log_info(f"{mobile}: login res {res.text}")
if res.status_code == 200:
rj = res.json()
if rj['code'] == 601:
tool.replyMsg(f"[{self.hide_phone(mobile)}]账号需要点击验证之后,重新登陆\n{rj['RiskUrl']}")
return False, None
if rj['code'] == 200:
tool.replyMsg(f"【登陆账户】:{self.hide_phone(mobile)}\n【JD账户】:{rj['pin']}\n【登陆结果】:{rj['message']}")
value_json = {
"mobile": mobile,
"pwd": pwd
}
sender.bucketSet("rabbit_zm_acc", rj['pin'], json.dumps(value_json))
return True, rj['pin']
elif rj['code'] == 503:
tool.replyMsg(f"[{self.hide_phone(mobile)}]{rj['message']}")
return False, None
elif rj['code'] == 555:
# 需要验证
riskUrl = rj['RiskUrl']
tool.replyMsg(f"[{self.hide_phone(mobile)}]请手动点击验证链接,然后再次返回登陆:{riskUrl}")
return False, None
else:
tool.replyMsg(f"[{self.hide_phone(mobile)}]登陆异常:{rj['message']}")
return False, None
def after_login(self, pin):
time.sleep(random.randint(3,5))
sender.bucketSet('pin' + imType.upper(), pin, userId)
qls = tool.qls()
if not qls or len(qls) == 0:
tool.pushMaster("未配置青龙容器,无法同步更新容器ck")
exit()
default_ql = list(filter(lambda item: self.conf['ql_name'] in item['name'],qls))
if not default_ql:
tool.pushMaster(f"容器【{self.conf['ql_name']}】,不存在,请重新配置")
return
tql = QL(default_ql[0])
if not tql.token:
return
res = tql.envGet("JD_COOKIE")
if res['code'] != 200 or len(res['data']) == 0:
return
if len(res['data']) <= 0:
return
cookie = list(filter(lambda item: pin in item['value'], res['data']))
if not cookie:
return
if cookie[0]['status'] == 1:
return
if not self.check_ck_status(cookie[0]['value']):
return
ck = cookie[0]['value']
pt_key = re.findall(r"pt_key=(.*?);", ck)[0]
val = {
"ID": pin,
"Pet": False,
"Fruit": False,
"DreamFactory": False,
"Note": "",
"PtKey": pt_key,
"AssetCron": "",
"PushPlus": "",
"LoginedAt": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"ClientID": tql.client_id
}
sender.bucketSet('jdNotify', pin, json.dumps(val))
for ql in qls:
if self.conf['ql_name'] in ql['name']:
continue
tql = QL(ql)
if not tql.token:
continue
res = tql.envGet("JD_COOKIE")
if res['code'] != 200 or len(res['data']) == 0:
continue
if len(res['data']) <= 0:
continue
cookie = list(filter(lambda item: pin in item['value'], res['data']))
if not cookie:
continue
ck = cookie[0]['value']
id = cookie[0].get('id', cookie[0].get("_id"))
remarks = cookie[0].get('remarks', pin)
if ck:
updateEnv = {
"name": "JD_COOKIE",
"value": ck,
"remarks": remarks,
"id": id,
}
res = tql.envUpdate(json.dumps(updateEnv))
tool.log_info(f"update res {res}")
res = tql.envEnable([id])
tool.log_info(f"envEnable res {res}")
def refresh_account(self):
need = 0
success = 0
failed = 0
failed_user = []
tool.log_info(f"[rabbit]账密刷新开始......")
acc_map = sender.bucketAll("rabbit_zm_acc")
if not acc_map:
sender.reply("暂无账密数据,已退出")
# 查询pin对应的ck是否有效
for pin, accStr in acc_map.items():
jdStr = sender.bucketGet("jdNotify", pin)
if not jdStr:
continue
jd_json = json.loads(jdStr)
ck = f"pt_pin={pin}; pt_key={jd_json['PtKey']}"
if self.check_ck_status(ck):
tool.log_info(f"账号【{pin}】有效✅,跳过刷新")
continue
need += 1
acc_json = json.loads(accStr)
refresh_res = self.run_login(acc_json['mobile'], acc_json['pwd'])
if not refresh_res:
failed += 1
failed_user.append(f"{quote(pin)}[{self.hide_phone(acc_json['mobile'])}]")
else:
success += 1
content = f"[rabbit]账密刷新结束"
if need > 0:
content += f"\n本次需要刷新:{need}"
content += f"\n本次刷新成功:{success}个🎉"
if failed_user:
content += f"\n本次刷新失败:{failed}个😭"
s = "\n".join(failed_user)
content += f"\n刷新失败用户:{s}"
else:
content += f"\n本次所有账户有效,无需刷新🎉"
tool.pushMaster(content)
def account_manage(self):
acc_map = sender.bucketAll("rabbit_zm_acc")
if not acc_map:
tool.replyMsg("暂无账密记录,请先登录")
exit()
content = f"账密列表如下:"
no = 0
for pin,accStr in acc_map.items():
acc_json = json.loads(accStr)
no +=1
content += f"\n{no}{quote(pin)}[{self.hide_phone(acc_json['mobile'])}]"
tool.replyMsg(content)
def run_login(self, mobile, pwd):
if not self.login_init(mobile):
return False
login_res, pin = self.login(mobile, pwd)
if not login_res:
return False
self.after_login(pin)
return True
if __name__ == "__main__":
sender = middleware.Sender(middleware.getSenderID())
plugin_ver = sender.getPluginVersion()
plugin_name = sender.getPluginName()
msg = sender.getMessage().strip('"')
imType = "fake" if sender.getImtype() == "cron" else sender.getImtype()
chatId = sender.getChatID()
userId = sender.getUserID()
isAdmin = sender.isAdmin()
username = sender.getUserName()
tool = TOOL()
#### 定时任务 #####
cron = middleware.Cron(None)
crons = list(filter(lambda item: "兔子账密刷新" in item['cmd'],cron.getCrons()))
if not crons:
tool.log_info(f"【兔子账密刷新】定时任务不存在,重新添加")
id = cron.addCron("0 0,3,5 * * *","兔子账密刷新",True,False,"兔子账密刷新","","","")
tool.log_info(f"add cron id = {id}")
#### 定时任务 ####
tz = ZM_RABBIT()
if "登录" in msg or "登陆" in msg:
sms_content = "请在60秒内输入手机号(q:退出)"
tool.replyMsg(sms_content)
mobile = sender.listen(60000)
if not mobile or mobile == "q" or mobile == "error":
tool.replyMsg("已退出!")
exit()
pattern = r'^1[3-9]\d{9}$'
if not re.match(pattern, mobile):
tool.replyMsg("手机号格式错误,已退出!")
exit()
tool.replyMsg("请在60秒内输入登录密码(q:退出)")
pwd = sender.listen(60000)
if not pwd or pwd == "q" or pwd == "error":
tool.replyMsg("已退出!")
exit()
if len(pwd) < 8:
tool.replyMsg("密码格式错误,已退出!")
exit()
tz.run_login(mobile, pwd)
exit()
if "刷新" in msg:
tz.refresh_account()
exit()
if "管理" in msg:
tz.account_manage()