Skip to content

Commit

Permalink
feat: supports gewechat login (hanfangyuan4396#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
hanfangyuan4396 authored Dec 3, 2024
1 parent 51d1d20 commit 97f7736
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 16 deletions.
73 changes: 62 additions & 11 deletions channel/gewechat/gewechat_channel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import json
import web
from urllib.parse import urlparse

from bridge.context import Context
from bridge.reply import Reply, ReplyType
Expand All @@ -10,7 +11,7 @@
from common.singleton import singleton
from common.tmp_dir import TmpDir
from common.utils import compress_imgfile, fsize
from config import conf
from config import conf, save_config
from lib.gewechat import GewechatClient

MAX_UTF8_LEN = 2048
Expand All @@ -21,22 +22,72 @@ class GeWeChatChannel(ChatChannel):

def __init__(self):
super().__init__()

self.base_url = conf().get("gewechat_base_url")
self.download_url = conf().get("gewechat_download_url")
if not self.base_url:
logger.error("[gewechat] base_url is not set")
return
self.token = conf().get("gewechat_token")
self.app_id = conf().get("gewechat_app_id")
logger.info(
"[gewechat] init: base_url: {}, download_url: {}, token: {}, app_id: {}".format(
self.base_url, self.download_url, self.token, self.app_id
)
)
self.client = GewechatClient(self.base_url, self.token)

# 如果token为空,尝试获取token
if not self.token:
logger.warning("[gewechat] token is not set,trying to get token")
token_resp = self.client.get_token()
# {'ret': 200, 'msg': '执行成功', 'data': 'tokenxxx'}
if token_resp.get("ret") != 200:
logger.error(f"[gewechat] get token failed: {token_resp}")
return
self.token = token_resp.get("data")
conf().set("gewechat_token", self.token)
save_config()
logger.info(f"[gewechat] new token saved: {self.token}")
self.client = GewechatClient(self.base_url, self.token)

self.app_id = conf().get("gewechat_app_id")
if not self.app_id:
logger.warning("[gewechat] app_id is not set,trying to get new app_id when login")

self.download_url = conf().get("gewechat_download_url")
if not self.download_url:
logger.warning("[gewechat] download_url is not set, unable to download image")

logger.info(f"[gewechat] init: base_url: {self.base_url}, token: {self.token}, app_id: {self.app_id}, download_url: {self.download_url}")

def startup(self):
urls = ("/v2/api/callback/collect", "channel.gewechat.gewechat_channel.Query")
# 如果app_id为空或登录后获取到新的app_id,保存配置
app_id, error_msg = self.client.login(self.app_id)
if error_msg:
logger.error(f"[gewechat] login failed: {error_msg}")
return

# 如果原来的self.app_id为空或登录后获取到新的app_id,保存配置
if not self.app_id or self.app_id != app_id:
conf().set("gewechat_app_id", app_id)
save_config()
logger.info(f"[gewechat] new app_id saved: {app_id}")
self.app_id = app_id

# 获取回调地址,示例地址:http://172.17.0.1:9919/v2/api/callback/collect
callback_url = conf().get("gewechat_callback_url")
if not callback_url:
logger.error("[gewechat] callback_url is not set, unable to start callback server")
return

# 设置回调地址,{ "ret": 200, "msg": "操作成功" }
callback_resp = self.client.set_callback(self.token, callback_url)
if callback_resp.get("ret") != 200:
logger.error(f"[gewechat] set callback failed: {callback_resp}")
return

# 从回调地址中解析出端口与url path,启动回调服务器
parsed_url = urlparse(callback_url)
path = parsed_url.path
port = parsed_url.port
logger.info(f"[gewechat] start callback server: {callback_url}")
urls = (path, "channel.gewechat.gewechat_channel.Query")
app = web.application(urls, globals(), autoreload=False)
port = conf().get("gewechat_callback_server_port", 9919)
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port))
web.httpserver.runsimple(app.wsgifunc(), ("0.0.0.0", port)) # 传入(host, port)元组

def send(self, reply: Reply, context: Context):
receiver = context["receiver"]
Expand Down
21 changes: 20 additions & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
"gewechat_download_url": "",
"gewechat_token": "",
"gewechat_app_id": "",
"gewechat_callback_server_port": 9919,
"gewechat_callback_url": "", # 回调地址,示例:http://172.17.0.1:9919/v2/api/callback/collect

# chatgpt指令自定义触发词
"clear_memory_commands": ["#清除记忆"], # 重置会话指令,必须以#开头
Expand Down Expand Up @@ -236,6 +236,12 @@ def get(self, key, default=None):
return default
except Exception as e:
raise e

def set(self, key, value):
try:
self[key] = value
except Exception as e:
raise e

# Make sure to return a dictionary to ensure atomic
def get_user_data(self, user) -> dict:
Expand Down Expand Up @@ -327,6 +333,19 @@ def load_config():

config.load_user_datas()

def save_config():
global config
config_path = "./config.json"
try:
config_dict = dict(config) # 将Config对象转换为普通字典
# 创建一个按键排序的有序字典
sorted_config = {key: config_dict[key] for key in sorted(config_dict.keys())}
with open(config_path, "w", encoding="utf-8") as f:
json.dump(sorted_config, f, indent=4, ensure_ascii=False)
logger.info("[Config] Configuration saved.")
except Exception as e:
logger.error(f"[Config] Save configuration error: {e}")


def get_root():
return os.path.dirname(os.path.abspath(__file__))
Expand Down
98 changes: 96 additions & 2 deletions lib/gewechat/api/login_api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from ..util.terminal_printer import make_and_print_qr, print_green, print_yellow, print_red
from ..util.http_util import post_json
import time


class LoginApi:
def __init__(self, base_url, token):
self.base_url = base_url
self.token = token

def get_token(self):
"""获取tokenId 将tokenId 配置到OkhttpUtil 类中的token 属性"""
"""获取tokenId"""
return post_json(self.base_url, "/tools/getTokenId", self.token, {})

def set_callback(self, token, callback_url):
Expand Down Expand Up @@ -59,4 +62,95 @@ def logout(self, app_id):
param = {
"appId": app_id
}
return post_json(self.base_url, "/login/logout", self.token, param)
return post_json(self.base_url, "/login/logout", self.token, param)

def _get_and_validate_qr(self, app_id):
"""获取并验证二维码数据
Args:
app_id: 应用ID
Returns:
tuple: (app_id, uuid) 或在失败时返回 (None, None)
"""

qr_response = self.get_qr(app_id)
if qr_response.get('ret') != 200:
print_yellow(f"获取二维码失败:", qr_response)
return None, None

qr_data = qr_response.get('data', {})
app_id = qr_data.get('appId')
uuid = qr_data.get('uuid')
if not app_id or not uuid:
print_yellow(f"app_id: {app_id}, uuid: {uuid}, 获取app_id或uuid失败")
return None, None

return app_id, uuid

def login(self, app_id):
"""执行完整的登录流程
Args:
app_id: 可选的应用ID,为空时会自动创建新的app_id
Returns:
tuple: (app_id: str, error_msg: str)
成功时 error_msg 为空字符串
失败时 app_id 可能为空字符串,error_msg 包含错误信息
"""
# 1. 检查是否已经登录
input_app_id = app_id
if input_app_id:
check_online_response = self.check_online(input_app_id)
if check_online_response.get('ret') == 200 and check_online_response.get('data'):
print_green(f"AppID: {input_app_id} 已在线,无需登录")
return input_app_id, ""
else:
print_yellow(f"AppID: {input_app_id} 未在线,执行登录流程")

# 2. 获取初始二维码
app_id, uuid = self._get_and_validate_qr(app_id)
if not app_id or not uuid:
return "", "获取二维码失败"

if not input_app_id:
print_green(f"AppID: {app_id}, 请保存此app_id,下次登录时继续使用!")
print_yellow("\n新设备登录平台,次日凌晨会掉线一次,重新登录时需使用原来的app_id取码,否则新app_id仍然会掉线,登录成功后则可以长期在线")

make_and_print_qr(f"http://weixin.qq.com/x/{uuid}")

# 3. 轮询检查登录状态
retry_count = 0
max_retries = 100 # 最大重试100次

while retry_count < max_retries:
login_status = self.check_qr(app_id, uuid, "")
if login_status.get('ret') != 200:
print_red(f"检查登录状态失败: {login_status}")
return app_id, f"检查登录状态失败: {login_status}"

login_data = login_status.get('data', {})
status = login_data.get('status')
expired_time = login_data.get('expiredTime', 0)

# 检查二维码是否过期,提前5秒重新获取
if expired_time <= 5:
print_yellow("二维码即将过期,正在重新获取...")
_, uuid = self._get_and_validate_qr(app_id)
if not uuid:
return app_id, "重新获取二维码失败"

make_and_print_qr(f"http://weixin.qq.com/x/{uuid}")
continue

if status == 2: # 登录成功
nick_name = login_data.get('nickName', '未知用户')
print_green(f"\n登录成功!用户昵称: {nick_name}")
return app_id, ""
else:
retry_count += 1
if retry_count >= max_retries:
print_yellow("登录超时,请重新尝试")
return False
time.sleep(5)
7 changes: 6 additions & 1 deletion lib/gewechat/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .api.contact_api import ContactApi
from .api.download_api import DownloadApi
from .api.download_api import DownloadApi
from .api.favor_api import FavorApi
from .api.group_api import GroupApi
from .api.label_api import LabelApi
Expand All @@ -15,7 +16,7 @@ class GewechatClient:
使用示例:
```
# 初始化客户端
client = GewechatClient("http://服务ip:2531/v2/api", "http://服务ip:2532/download", "your_token_here")
client = GewechatClient("http://服务ip:2531/v2/api", "your_token_here")
app_id = "your_app_id"
# 获取联系人列表
contacts = client.fetch_contacts_list(app_id)
Expand Down Expand Up @@ -243,6 +244,10 @@ def update_head_img(self, app_id, head_img_url):
return self._personal_api.update_head_img(app_id, head_img_url)

# Login API methods
def login(self, app_id):
"""登录"""
return self._login_api.login(app_id)

def get_token(self):
"""获取tokenId"""
return self._login_api.get_token()
Expand Down
2 changes: 1 addition & 1 deletion lib/gewechat/util/http_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ def post_json(base_url, route, token, data):
else:
raise RuntimeError(response.text)
except Exception as e:
print(f"url={url}, exception={e}")
print(f"http请求失败, url={url}, exception={e}")
raise RuntimeError(str(e))
31 changes: 31 additions & 0 deletions lib/gewechat/util/terminal_printer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import qrcode

def print_green(text):
print(f"\033[32m{text}\033[0m")

def print_yellow(text):
print(f"\033[33m{text}\033[0m")

def print_red(text):
print(f"\033[31m{text}\033[0m")

def make_and_print_qr(url):
"""生成并打印二维码
Args:
url: 需要生成二维码的URL字符串
Returns:
None
功能:
1. 在终端打印二维码的ASCII图形
2. 同时提供在线二维码生成链接作为备选
"""
print_green("请扫描下方二维码登录")
qr = qrcode.QRCode()
qr.add_data(url)
qr.make()
qr.print_ascii(invert=True)
print_green(f"也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}")

0 comments on commit 97f7736

Please sign in to comment.