From dd59c2bb00a1a94ee23f5b1c863427aa16f1ee06 Mon Sep 17 00:00:00 2001 From: hitmant Date: Fri, 28 Feb 2025 14:51:12 +0800 Subject: [PATCH] =?UTF-8?q?POE2=E6=B6=88=E6=81=AF=E8=BD=AC=E5=8F=91?= =?UTF-8?q?=EF=BC=8C=E5=88=9D=E5=A7=8B=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 61 ++++++++++ build.py | 15 +++ config.py | 89 ++++++++++++++ main.py | 299 ++++++++++++++++++++++++++++++++++++++++++++++ message_sender.py | 38 ++++++ requirements.txt | 4 + 6 files changed, 506 insertions(+) create mode 100644 .gitignore create mode 100644 build.py create mode 100644 config.py create mode 100644 main.py create mode 100644 message_sender.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f950b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec +build/ +dist/ + +# Logs +*.log +logs/ +poe_forwarder.log + +# Config +config.json + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# Virtual Environment +venv/ +.env/ +.venv/ +ENV/ \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..4617fc9 --- /dev/null +++ b/build.py @@ -0,0 +1,15 @@ +import PyInstaller.__main__ +import os + +# 确保在脚本所在目录运行 +os.chdir(os.path.dirname(os.path.abspath(__file__))) + +PyInstaller.__main__.run([ + 'main.py', # 主程序文件 + '--onefile', # 打包成单个文件 + '--name=POE_Message_Forwarder', # 输出文件名 + '--clean', # 清理临时文件 + '--noconsole', # 不显示控制台窗口 + '--hidden-import=PIL._tkinter', # 确保PIL正确导入 + '--hidden-import=PIL._imaging', +]) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..52f8425 --- /dev/null +++ b/config.py @@ -0,0 +1,89 @@ +import os +import json +import sys +import tkinter as tk +from tkinter import messagebox + +# 默认配置 +DEFAULT_CONFIG = { + # Path of Exile客户端日志路径 + "CLIENT_LOG_PATH": "C:/Program Files (x86)/Grinding Gear Games/Path of Exile 2/logs/Client.txt", + + # 消息发送API配置 + "API_URL": "https://al.xj.rs/api/v1/send", + "API_KEY": "", # 需要用户填写 + + # 交易通知配置 + "TRADE_MESSAGES_CONFIG": { + "enabled": True, # 是否启用交易通知 + "message_forward": True, # 是否转发到消息服务 + }, + + # 私聊消息相关配置 + "CHAT_MESSAGES": { + "enabled": True, # 是否启用私聊转发 + "message_forward": True, # 是否转发到消息服务 + } +} + +CONFIG_FILE = "config.json" + +def show_error(title, message): + """显示错误消息对话框""" + root = tk.Tk() + root.withdraw() # 隐藏主窗口 + messagebox.showerror(title, message) + root.destroy() + sys.exit(1) + +def load_config(): + """加载或创建配置文件""" + if not os.path.exists(CONFIG_FILE): + # 创建默认配置文件 + try: + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(DEFAULT_CONFIG, f, indent=4, ensure_ascii=False) + show_error( + "配置文件创建成功", + f"已创建默认配置文件: {CONFIG_FILE}\n请编辑配置文件填写必要信息后重启程序" + ) + except Exception as e: + show_error( + "错误", + f"创建配置文件失败: {str(e)}" + ) + + try: + # 读取配置文件 + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + + # 检查必要的配置项 + if not config.get("API_KEY"): + show_error( + "配置错误", + "请在配置文件中设置API_KEY后重启程序" + ) + + return config + + except json.JSONDecodeError: + show_error( + "配置错误", + "配置文件格式错误,请检查JSON格式是否正确" + ) + except Exception as e: + show_error( + "错误", + f"读取配置文件失败: {str(e)}" + ) + +# 加载配置 +config = load_config() + +# 导出配置项 +CLIENT_LOG_PATH = config["CLIENT_LOG_PATH"] +API_URL = config["API_URL"] +API_KEY = config["API_KEY"] +TRADE_MESSAGES_CONFIG = config["TRADE_MESSAGES_CONFIG"] +CHAT_MESSAGES = config["CHAT_MESSAGES"] diff --git a/main.py b/main.py new file mode 100644 index 0000000..ace2a01 --- /dev/null +++ b/main.py @@ -0,0 +1,299 @@ +import os +import time +import re +import logging +from config import CLIENT_LOG_PATH, CHAT_MESSAGES, TRADE_MESSAGES_CONFIG +from message_sender import MessageSender +import threading +import signal +import pystray +from PIL import Image, ImageDraw +import tempfile + +# 配置日志 +def setup_logging(): + log_file = "poe_forwarder.log" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file, encoding='utf-8'), + logging.StreamHandler() + ] + ) + return logging.getLogger(__name__) + +def create_icon(): + """创建一个更美观的图标""" + width = 64 + height = 64 + + # 创建新图像,使用RGBA支持透明 + image = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + dc = ImageDraw.Draw(image) + + # 定义颜色 + primary_color = (65, 105, 225) # 皇家蓝 + secondary_color = (30, 144, 255) # 道奇蓝 + + # 绘制主圆形 + dc.ellipse([4, 4, width-4, height-4], fill=primary_color) + + # 绘制内部装饰 + # 绘制字母"P" + font_size = 32 + try: + from PIL import ImageFont + font = ImageFont.truetype("arial.ttf", font_size) + except: + font = None + + text = "P" + text_bbox = dc.textbbox((0, 0), text, font=font) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + + x = (width - text_width) // 2 + y = (height - text_height) // 2 + + dc.text((x, y), text, fill='white', font=font) + + return image + +class TrayIcon: + def __init__(self, logger): + self.logger = logger + self.running = True + self.icon = None + self.create_icon() + + def create_icon(self): + image = create_icon() + menu = ( + pystray.MenuItem( + "POE消息转发器", + lambda: None, + enabled=False + ), + pystray.Menu.SEPARATOR, + pystray.MenuItem( + "✓ 正在运行", + lambda: None, + enabled=False + ), + pystray.MenuItem( + "📝 查看日志", + self.open_log + ), + pystray.Menu.SEPARATOR, + pystray.MenuItem( + "⚙ 打开配置", + self.open_config + ), + pystray.MenuItem( + "❌ 退出", + self.stop_application + ) + ) + + self.icon = pystray.Icon( + "poe_forwarder", + image, + "POE消息转发器", + menu + ) + + def open_log(self): + """打开日志文件""" + try: + os.startfile("poe_forwarder.log") + except Exception as e: + self.logger.error(f"打开日志文件失败: {str(e)}") + + def open_config(self): + """打开配置文件""" + try: + os.startfile("config.json") + except Exception as e: + self.logger.error(f"打开配置文件失败: {str(e)}") + + def stop_application(self): + """停止应用""" + self.logger.info("从托盘菜单退出程序") + self.running = False + self.icon.stop() + + def run(self): + """运行托盘图标""" + self.icon.run() + +class TradeNotificationHandler: + def __init__(self, message_sender, logger): + self.sender = message_sender + self.last_position = 0 + self.logger = logger + + def check_new_trades(self): + try: + with open(CLIENT_LOG_PATH, 'r', encoding='utf-8', errors='ignore') as f: + # 获取当前文件大小 + f.seek(0, 2) + current_position = f.tell() + + # 如果文件大小变小了,说明文件被重置 + if current_position < self.last_position: + self.logger.info(f"检测到文件重置") + self.last_position = 0 # 重置到文件开始 + + # 如果有新内容 + if current_position > self.last_position: + # 只读取新增的内容 + f.seek(self.last_position) + new_content = f.read() + + # 更新位置(在处理内容之前) + self.last_position = current_position + + # 按行处理新内容 + lines = new_content.splitlines() + for line in lines: + if '@來自' in line or '@From' in line: + self.logger.info(f"发现新消息: {line[:100]}...") + self.process_trade_message(line) + + except Exception as e: + self.logger.error(f"读取日志文件时发生错误: {str(e)}") + + def process_trade_message(self, message): + self.logger.info("\n开始处理消息...") + + # 首先判断消息类型 + if '我想購買' in message or 'would like to buy' in message: + self.logger.info("检测到交易消息") + trade_patterns = [ + # 繁体中文格式 + { + 'pattern': r'.*\[INFO Client \d+\] @來自 ([^:]+): [^,]*,我想購買 ([^標]+)標價 ([^在]+)在 [^(]*\(倉庫頁 "([^"]+)"; 位置: ([^)]+)\)', + 'format': lambda m: { + 'buyer': m.group(1), + 'item': m.group(2).strip(), + 'price': m.group(3).strip(), + 'tab': m.group(4), + 'position': m.group(5) + } + }, + # 英文格式 + { + 'pattern': r'.*\[INFO Client \d+\] @From ([^:]+): Hi, I would like to buy your ([^listed]+)listed for ([^in]+)in [^(]*\(stash tab "([^"]+)"; position: ([^)]+)\)', + 'format': lambda m: { + 'buyer': m.group(1), + 'item': m.group(2).strip(), + 'price': m.group(3).strip(), + 'tab': m.group(4), + 'position': m.group(5) + } + } + ] + + for pattern_info in trade_patterns: + match = re.match(pattern_info['pattern'], message) + if match: + self.logger.info("匹配到交易消息模式") + trade_info = pattern_info['format'](match) + self.logger.info(f"解析的交易信息: {trade_info}") + + # 发送交易通知 + if TRADE_MESSAGES_CONFIG['message_forward'] and self.sender: + message_content = ( + f"买家: {trade_info['buyer']}\n" + f"商品: {trade_info['item']}\n" + f"价格: {trade_info['price']}\n" + f"仓库: {trade_info['tab']}\n" + f"位置: {trade_info['position']}" + ) + self.sender.send_message( + content=message_content, + title="POE 交易通知" + ) + break + else: + self.logger.info("检测到私聊消息") + # 处理私聊消息 + chat_patterns = [ + # 繁体中文格式 + r'.*\[INFO Client \d+\] @來自 ([^:]+): (.*)', + # 英文格式 + r'.*\[INFO Client \d+\] @From ([^:]+): (.*)' + ] + + for pattern in chat_patterns: + match = re.match(pattern, message) + if match: + sender = match.group(1) + content = match.group(2) + + # 发送私聊消息 + if CHAT_MESSAGES['message_forward']: + message_content = ( + f"发送者:{sender}\n" + f"内容:{content}\n" + f"时间:{time.strftime('%H:%M:%S')}" + ) + self.sender.send_message( + content=message_content, + title="POE 私聊消息" + ) + break + +def monitor_file(handler, is_running): + handler.logger.info("文件监控已启动") + try: + while is_running(): + handler.check_new_trades() + time.sleep(0.1) + except Exception as e: + handler.logger.error(f"监控线程异常: {str(e)}") + handler.logger.info("监控线程已停止") + +def main(): + logger = setup_logging() + logger.info("程序启动...") + + # 创建托盘图标 + tray = TrayIcon(logger) + + logger.info("初始化消息发送器...") + message_sender = MessageSender() + + logger.info(f"开始监控日志文件: {CLIENT_LOG_PATH}") + handler = TradeNotificationHandler(message_sender, logger) + + # 确保文件存在 + if not os.path.exists(CLIENT_LOG_PATH): + logger.error(f"错误:找不到日志文件 {CLIENT_LOG_PATH}") + return + + try: + with open(CLIENT_LOG_PATH, 'r', encoding='utf-8', errors='ignore') as f: + f.seek(0, 2) + handler.last_position = f.tell() + logger.info(f"初始文件位置: {handler.last_position}") + except Exception as e: + logger.error(f"读取文件位置时发生错误: {str(e)}") + return + + # 在新线程中运行文件监控 + monitor_thread = threading.Thread( + target=monitor_file, + args=(handler, lambda: tray.running) + ) + monitor_thread.daemon = True + monitor_thread.start() + + # 运行托盘图标(这会阻塞主线程) + tray.run() + + logger.info("程序已退出") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/message_sender.py b/message_sender.py new file mode 100644 index 0000000..1ee8e97 --- /dev/null +++ b/message_sender.py @@ -0,0 +1,38 @@ +import requests +from config import API_KEY, API_URL + +class MessageSender: + def __init__(self): + self.api_url = API_URL + self.api_key = API_KEY + + def send_message(self, content: str, title: str = "POE 通知") -> bool: + """ + 发送消息 + :param content: 消息内容 + :param title: 消息标题 + :return: 是否发送成功 + """ + try: + # 完全使用与示例代码相同的结构 + response = requests.post( + self.api_url, + headers={ + "X-API-Key": self.api_key, + "Content-Type": "application/json" + }, + json={ + "content": content, + "title": title + } + ) + + print(f"\n发送消息结果:") + print(f"状态码: {response.status_code}") + print(f"响应: {response.json()}") + + return response.status_code == 200 + + except Exception as e: + print(f"发送消息失败: {str(e)}") + return False \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3134645 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests==2.31.0 +pyinstaller==6.3.0 +pillow==10.2.0 +pystray==0.19.5 \ No newline at end of file