From 9937115ad39a996c44f6efd6ba7ba456a8c89bdb Mon Sep 17 00:00:00 2001 From: liuwei Date: Wed, 5 Mar 2025 09:01:08 +0800 Subject: [PATCH] =?UTF-8?q?feature=EF=BC=9A1.=E7=82=B9=E6=AD=8C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C2.=E7=AD=BE=E5=88=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- base/func_news.py | 31 ---- base/func_report_reminder.py | 8 +- config.yaml | 21 +++ configuration.py | 4 + group_auto/group_member_change.py | 26 ++-- main.py | 14 +- message_sign/config.toml | 7 + message_sign/main.py | 233 ++++++++++++++++++++++++++++++ message_storage/message_to_db.py | 183 +++++++++++------------ music/bot_music.py | 63 ++++++++ music/config.toml | 8 + requirements.txt | 6 +- robot.py | 119 ++++++++------- robot_cmd/robot_command.py | 1 + 14 files changed, 524 insertions(+), 200 deletions(-) create mode 100644 message_sign/config.toml create mode 100644 message_sign/main.py create mode 100644 music/bot_music.py create mode 100644 music/config.toml diff --git a/base/func_news.py b/base/func_news.py index 2817e70..dcf27ce 100644 --- a/base/func_news.py +++ b/base/func_news.py @@ -47,37 +47,6 @@ class News(object): return f"{fmt_time} {self.week[weekday_news]}\n{fmt_news}" - - def get_36kr_news(self): - url = "https://orz.ai/dailynews/?platform=36kr" - # 获取当前日期和英文星期名 - now = datetime.now() - current_date = now.strftime("%Y年%m月%d日") - english_weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] - chinese_weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] - - # 将英文星期名映射为中文 - current_weekday_index = now.weekday() # 获取当前是星期几(0代表星期一,6代表星期日) - current_weekday_chinese = chinese_weekdays[current_weekday_index] - - # 初始化一个空字符串来存储结果 - output = f"当前日期:{current_date} {current_weekday_chinese}\n\n" - - response = requests.get(url) - - if response.status_code == 200: - post = response.json() - str = post['data'] - # 遍历列表,并格式化每个字典的title, url,然后添加到output字符串中 - for index, article in enumerate(str, start=1): - title = article['title'] - url = article['url'] - # 使用f-string格式化字符串,并添加到output中 - output += f"{index}. 标题: {title}\n URL: {url}\n" - - # 输出最终的字符串(这里只是为了展示,实际上你可以根据需要处理这个字符串) - return output - def get_baidu_news(self): url = "https://top.baidu.com/api/board?platform=wise&tab=realtime" # 获取当前日期和英文星期名 diff --git a/base/func_report_reminder.py b/base/func_report_reminder.py index ce0661a..f30b1ba 100644 --- a/base/func_report_reminder.py +++ b/base/func_report_reminder.py @@ -18,16 +18,16 @@ class ReportReminder: today = datetime.datetime.now().date() # 如果是非工作日 if not is_workday(today): - robot.sendTextMsg("休息日快乐", receiver) + robot.send_text_msg("休息日快乐", receiver) # 如果是工作日 if is_workday(today): - robot.sendTextMsg("该发日报啦", receiver) + robot.send_text_msg("该发日报啦", receiver) # 如果是本周最后一个工作日 if ReportReminder.last_work_day_of_week(today) == today: - robot.sendTextMsg("该发周报啦", receiver) + robot.send_text_msg("该发周报啦", receiver) # 如果本日是本月最后一整周的最后一个工作日: if ReportReminder.last_work_friday_of_month(today) == today: - robot.sendTextMsg("该发月报啦", receiver) + robot.send_text_msg("该发月报啦", receiver) # 计算本月最后一个周的最后一个工作日 @staticmethod diff --git a/config.yaml b/config.yaml index 8acc001..4daf893 100644 --- a/config.yaml +++ b/config.yaml @@ -120,3 +120,24 @@ doubao: - 问题评价:分析问题的提出角度,如(财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐)等 - 总结:经过300个字以内的优化返回,返回内容请进行一定程度的结构化,方便快速阅读' # 根据需要对角色进行设定 + + +# config.yaml + +db_config: + pool_name: "wechat_boot_pool" + pool_size: 10 + host: "192.168.2.32" + user: "root" + password: "lw123456" + database: "message_archive" + charset: "utf8mb4" + use_unicode: true + get_warnings: true + pool_reset_session: true + +redis_config: + host: "192.168.2.32" + port: 6379 + db: 0 + decode_responses: true \ No newline at end of file diff --git a/configuration.py b/configuration.py index 691e7fc..959b193 100644 --- a/configuration.py +++ b/configuration.py @@ -40,3 +40,7 @@ class Config(object): self.CLAUDE = yconfig.get("claude", {}) self.DEEPSEEK = yconfig.get("deepseek", {}) self.DOUBAO = yconfig.get("doubao", {}) + + # DB config + self.mariadb = yconfig.get("db_config", {}) + self.redis = yconfig.get("redis_config", {}) diff --git a/group_auto/group_member_change.py b/group_auto/group_member_change.py index 0133be1..7650da2 100644 --- a/group_auto/group_member_change.py +++ b/group_auto/group_member_change.py @@ -4,15 +4,18 @@ import xml.etree.ElementTree as ET from wcferry import Wcf - class GroupMemberChange: - def __init__(self, wcf: Wcf): + def __init__(self, wcf: Wcf, redis_pool: redis.ConnectionPool): self.wcf = wcf # 假设 wcf 对象在此类中初始化 - self.r = redis.Redis(host='192.168.2.32', port=6379, db=0, decode_responses=True) + self.redis_pool = redis_pool # 初始化本地缓存字典,使用 group_id 作为键 self.local_membercounts = {} self.local_members = {} + def _get_redis_connection(self): + """从连接池获取 Redis 连接""" + return redis.Redis(connection_pool=self.redis_pool) + def get_current_members(self, group_id): """ 获取当前群成员信息 """ print(f"Fetching current members for group_id: {group_id}") @@ -27,8 +30,8 @@ class GroupMemberChange: # 读取 Redis 中的数据 print(f"Fetching previous data from Redis for group_id: {group_id}") - membercount_previous = self.r.get(membercount_key) - members_previous = self.r.hgetall(members_key) # 获取上次的成员信息 + membercount_previous = self._get_redis_connection().get(membercount_key) + members_previous = self._get_redis_connection().hgetall(members_key) # 获取上次的成员信息 print(f"Previous membercount: {membercount_previous}, Previous members: {members_previous}") return membercount_previous, members_previous @@ -48,9 +51,10 @@ class GroupMemberChange: if membercount_previous is None or not members_previous: print("First time processing, saving current data to Redis") members_current = self.get_current_members(group_id) - self.r.set(f"group:group_member_count:{group_id}", membercount_current) - self.r.delete(f"group:group_members:{group_id}") - self.r.hset(f"group:group_members:{group_id}", mapping=members_current) # 存储当前成员信息 + self._get_redis_connection().set(f"group:group_member_count:{group_id}", membercount_current) + self._get_redis_connection().delete(f"group:group_members:{group_id}") + self._get_redis_connection().hset(f"group:group_members:{group_id}", + mapping=members_current) # 存储当前成员信息 # 更新本地缓存 self.local_membercounts[group_id] = membercount_current @@ -99,11 +103,11 @@ class GroupMemberChange: # 更新 Redis 数据 print(f"Updating Redis with current membercount and members") - self.r.set(f"group:group_member_count:{group_id}", membercount_current) + self._get_redis_connection.set(f"group:group_member_count:{group_id}", membercount_current) - self.r.delete(f"group:group_members:{group_id}") + self._get_redis_connection.delete(f"group:group_members:{group_id}") # 更新 Redis 中的成员信息,确保在成员变化时也更新 - self.r.hset(f"group:group_members:{group_id}", mapping=members_current) + self._get_redis_connection.hset(f"group:group_members:{group_id}", mapping=members_current) else: result.append("$NO_CHANGE$") diff --git a/main.py b/main.py index a9908f8..e0b386b 100644 --- a/main.py +++ b/main.py @@ -25,25 +25,25 @@ def main(chat_type: int): robot.LOG.info(f"WeChatRobot【{__version__}】成功启动···") # 机器人启动发送测试消息 - robot.sendTextMsg("机器人启动成功!", "filehelper") + robot.send_text_msg("机器人启动成功!", "filehelper") # 接收消息 # robot.enableRecvMsg() # 可能会丢消息? robot.enableReceivingMsg() # 加队列 # 每天 8:30 发送新闻 - robot.onEveryTime("08:30", robot.newsBaiduReportAuto) + robot.onEveryTime("08:30", robot.news_baidu_report_auto) # epic - robot.onEveryTime("10:30", robot.sendEpicFreeGames) + robot.onEveryTime("10:30", robot.send_epic_free_games) # message report 1:数据自动从redis 转到sqllite - robot.onEveryTime("00:30", robot.messageCountToDB) + robot.onEveryTime("00:30", robot.message_count_to_db) # 从db中提取并发送给相关群 - robot.onEveryTime("09:30", robot.generateAndSendRanking) + robot.onEveryTime("09:30", robot.generate_and_send_ranking) # sehuatang - robot.onEveryTime("15:00", robot.generateSehuatangPdf) + robot.onEveryTime("15:00", robot.generate_sehuatang_pdf) # 游戏的定时任务每小时执行 robot.onEveryTime("18:00", robot.game_auto_tasks) @@ -55,7 +55,7 @@ def main(chat_type: int): robot.onEveryTime("17:30", robot.xiu_ren_pdf_send) # 让机器人一直跑 - robot.keepRunningAndBlockProcess() + robot.keep_running_and_block_process() if __name__ == "__main__": diff --git a/message_sign/config.toml b/message_sign/config.toml new file mode 100644 index 0000000..8bfce4b --- /dev/null +++ b/message_sign/config.toml @@ -0,0 +1,7 @@ +[SignIn] +enable = true +command = ["签到", "每日签到", "qd", "Qd", "QD"] +min-point = 3 +max-point = 20 +streak-cycle = 5 # 每签到?天后,额外积分奖励加1点? +max-streak-point = 10 # 额外积分奖励上限 \ No newline at end of file diff --git a/message_sign/main.py b/message_sign/main.py new file mode 100644 index 0000000..3de12c6 --- /dev/null +++ b/message_sign/main.py @@ -0,0 +1,233 @@ +import datetime +import logging +import mysql.connector.pooling +import tomllib +import pytz +import redis +from typing import Optional, Tuple + +from wcferry import Wcf, WxMsg +from robot_cmd.robot_command import GroupBotManager, Feature, PermissionStatus + +# 创建表的SQL语句 +CREATE_TABLE_SQL = """ +CREATE TABLE IF NOT EXISTS t_sign_record ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + wx_id VARCHAR(100) NOT NULL, + group_id VARCHAR(100) NOT NULL, + wx_nick_name VARCHAR(100) NOT NULL, + points INT DEFAULT 0, + sign_stat DATETIME, + signin_streak INT DEFAULT 0, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY unique_sign (wx_id, group_id) +) +""" + + +class SignInSystem: + def __init__(self, wcf: Wcf, gbm: GroupBotManager, all_contacts: dict, + db_pool: mysql.connector.pooling.MySQLConnectionPool, redis_pool: redis.ConnectionPool): + # 读取配置文件 + with open('message_sign/config.toml', 'rb') as f: + self.config = tomllib.load(f)['SignIn'] + + self.LOG = logging.getLogger(__name__) + + if not self.config['enable']: + raise Exception("签到功能未启用") + + self.wcf = wcf + self.gbm = gbm + self.all_contacts = all_contacts + self.db_pool = db_pool + self.redis_pool = redis_pool + self.command = self.config['command'] + self.min_point = self.config['min-point'] + self.max_point = self.config['max-point'] + self.streak_cycle = self.config['streak-cycle'] + self.max_streak_point = self.config['max-streak-point'] + # 时区设置 + self.timezone = 'Asia/Shanghai' + + # 从 Redis 初始化签到数据 + self.today_signin_count = self._load_signin_count_from_redis() + with self._get_redis_connection() as redis_client: + last_reset_date_str = redis_client.get('group:sign_in:last_reset_date') + if last_reset_date_str: + self.last_reset_date = datetime.datetime.strptime(last_reset_date_str, '%Y-%m-%d').date() + else: + self.last_reset_date = datetime.datetime.now(tz=pytz.timezone(self.timezone)).date() + self._save_last_reset_date_to_redis() + + self.LOG.info(f"[签到] 组件初始化完成 {self.command_format}") + + def _get_db_connection(self): + """从连接池获取数据库连接""" + return self.db_pool.get_connection() + + def _get_redis_connection(self): + """从连接池获取 Redis 连接""" + return redis.Redis(connection_pool=self.redis_pool) + + def _load_signin_count_from_redis(self) -> dict: + """从 Redis 加载签到人数数据""" + signin_count = {} + with self._get_redis_connection() as redis_client: + keys = redis_client.keys('group:sign_in:*') + for key in keys: + if key == 'group:sign_in:last_reset_date': + continue + group_id = key.replace('group:sign_in:', '') + count = redis_client.get(key) + if count is not None: + signin_count[group_id] = int(count) + return signin_count + + def _save_signin_count_to_redis(self, group_id: str, count: int): + """保存签到人数到 Redis""" + with self._get_redis_connection() as redis_client: + redis_client.set(f'group:sign_in:{group_id}', count) + + def _save_last_reset_date_to_redis(self): + """保存最后重置日期到 Redis""" + with self._get_redis_connection() as redis_client: + redis_client.set('group:sign_in:last_reset_date', self.last_reset_date.strftime('%Y-%m-%d')) + + @property + def command_format(self): + return ','.join(self.command) + + @property + def enable(self): + return self.config['enable'] + + def initialize_table(self): + """初始化数据库表""" + with self._get_db_connection() as conn: + with conn.cursor(dictionary=True) as cursor: # 使用 dictionary=True 返回字典格式 + cursor.execute(CREATE_TABLE_SQL) + conn.commit() + + def reset_today_count_if_needed(self): + """检查并重置每日签到计数""" + current_date = datetime.datetime.now(tz=pytz.timezone(self.timezone)).date() + if current_date != self.last_reset_date: + self.today_signin_count.clear() + with self._get_redis_connection() as redis_client: + keys = redis_client.keys('group:sign_in:*') + for key in keys: + if key != 'group:sign_in:last_reset_date': + redis_client.delete(key) + self.last_reset_date = current_date + self._save_last_reset_date_to_redis() + self.LOG.info(f"[签到] 已重置每日签到计数,日期更新为 {current_date}") + + def get_today_signin_count(self, group_id: str) -> int: + """获取群内今日签到人数(使用缓存)""" + self.reset_today_count_if_needed() + return self.today_signin_count.get(group_id, 0) + + def get_user_record(self, wx_id: str, group_id: str) -> Optional[dict]: + """获取用户签到记录""" + with self._get_db_connection() as conn: + with conn.cursor(dictionary=True) as cursor: + query = """ + SELECT wx_id, group_id, wx_nick_name, points, sign_stat, signin_streak + FROM t_sign_record + WHERE wx_id = %s AND group_id = %s + """ + cursor.execute(query, (wx_id, group_id)) + return cursor.fetchone() + + def calculate_points(self, streak: int) -> int: + """根据连续签到天数计算积分""" + base_points = self.min_point + extra_points = min(streak // self.streak_cycle, self.max_streak_point) + total_points = base_points + extra_points + return min(total_points, self.max_point) + + def member_sign_in(self, message: WxMsg): + """会员签到功能""" + if not self.enable: + return + + content = str(message.content).strip() + command = content.split(" ") + if not len(command) or command[0] not in self.command: + return + + if self.gbm.get_group_permission(message.roomid, Feature.TASK_GAME) == PermissionStatus.DISABLED: + return + + current_time = datetime.datetime.now(tz=pytz.timezone(self.timezone)) + today_start = current_time.replace(hour=0, minute=0, second=0, microsecond=0) + yesterday = today_start - datetime.timedelta(days=1) + + user_record = self.get_user_record(message.sender, message.roomid) + wx_nick_name = self.all_contacts.get(message.sender, message.sender) + + if user_record and user_record['sign_stat'] and user_record['sign_stat'] >= today_start: + self.wcf.send_text( + f"@{wx_nick_name} 您今天已经签到过了!当前积分:{user_record['points']}", + message.sender + ) + return + + streak = 0 + if user_record and user_record['sign_stat']: + last_sign_date = user_record['sign_stat'].replace(hour=0, minute=0, second=0, microsecond=0) + if last_sign_date == yesterday: + streak = user_record['signin_streak'] + 1 + else: + streak = 1 + else: + streak = 1 + + today_signin_rank = self.get_today_signin_count(message.roomid) + 1 + self.today_signin_count[message.roomid] = today_signin_rank + self._save_signin_count_to_redis(message.roomid, today_signin_rank) + + points_to_add = self.calculate_points(streak) + + with self._get_db_connection() as conn: + with conn.cursor(dictionary=True) as cursor: + if user_record: + update_sql = """ + UPDATE t_sign_record + SET wx_nick_name = %s, points = points + %s, + sign_stat = %s, signin_streak = %s, + update_time = %s + WHERE wx_id = %s AND group_id = %s + """ + cursor.execute(update_sql, ( + wx_nick_name, points_to_add, current_time, streak, + current_time, message.sender, message.roomid + )) + else: + insert_sql = """ + INSERT INTO t_sign_record + (wx_id, group_id, wx_nick_name, points, sign_stat, signin_streak) + VALUES (%s, %s, %s, %s, %s, %s) + """ + cursor.execute(insert_sql, ( + message.sender, message.roomid, wx_nick_name, points_to_add, current_time, streak + )) + conn.commit() + + msg = ( + f"@{wx_nick_name} 签到成功!\n" + f"您是今日群内第{today_signin_rank}个签到的\n" + f"连续签到{streak}天,本次获得{points_to_add}积分" + ) + self.wcf.send_text( + msg, + (message.roomid if message.from_group() else message.sender), + message.sender + ) + + def __del__(self): + """连接池由外部管理,不需要手动关闭""" + pass + diff --git a/message_storage/message_to_db.py b/message_storage/message_to_db.py index 58f2ee1..3cf1bf1 100644 --- a/message_storage/message_to_db.py +++ b/message_storage/message_to_db.py @@ -4,120 +4,109 @@ import redis import xml.etree.ElementTree as ET from message_summary.message_summary_4o import message_summary -# 配置数据库连接 -db_config = { - 'host': '192.168.2.32', # 替换为你的MariaDB服务器地址 - 'user': 'root', # 替换为你的MariaDB用户名 - 'password': 'lw123456', # 替换为你的MariaDB密码 - 'database': 'message_archive' -} - -# 连接到Redis -r = redis.Redis(host='192.168.2.32', port=6379, db=0) +import mysql.connector.pooling -def archive_message(group_id, timestamp_str, sender, content, message_type, attachment_url=None): - # 连接到数据库 - connection = pymysql.connect(**db_config) +class MessageStorage: - try: - with connection.cursor() as cursor: - # 插入消息信息 - sql = """ - INSERT INTO messages (group_id,timestamp, sender, content, message_type, attachment_url) - VALUES (%s, %s, %s, %s, %s, %s) - """ - cursor.execute(sql, (group_id, timestamp_str, sender, content, message_type, attachment_url)) + def __init__(self, db_pool: mysql.connector.pooling.MySQLConnectionPool, redis_pool: redis.ConnectionPool): + self.redis_pool = redis_pool + self.db_pool = db_pool + # 初始化本地缓存字典,使用 group_id 作为键 + self.local_membercounts = {} + self.local_members = {} - # 提交事务 - connection.commit() - print(f"Archived:{timestamp_str}:{group_id}:{sender}: {content}") + def _get_redis_connection(self): + """从连接池获取 Redis 连接""" + return redis.Redis(connection_pool=self.redis_pool) - except Exception as e: - print(f"Error archiving message: {e}") - connection.rollback() + def archive_message(self, group_id, timestamp_str, sender, content, message_type, attachment_url=None): + # 连接到数据库 + connection = self.db_pool.get_connection() - finally: - # 关闭连接 - connection.close() + try: + with connection.cursor() as cursor: + # 插入消息信息 + sql = """ + INSERT INTO messages (group_id,timestamp, sender, content, message_type, attachment_url) + VALUES (%s, %s, %s, %s, %s, %s) + """ + cursor.execute(sql, (group_id, timestamp_str, sender, content, message_type, attachment_url)) + # 提交事务 + connection.commit() + print(f"Archived:{timestamp_str}:{group_id}:{sender}: {content}") -def get_messages(group_id, all_contacts: dict): - # 连接到数据库 - with pymysql.connect(**db_config) as connection: - # 获取 redis 中的上次总结时间,本次从上次开始算,若没有,则从 8 小时之前开始计算 - key = f"{group_id}:summary_time" - last_summary_time = r.get(key) + except Exception as e: + print(f"Error archiving message: {e}") + connection.rollback() - # 如果 Redis 返回值为字节类型,转换为字符串 - if last_summary_time: - last_summary_time = last_summary_time.decode('utf-8') + finally: + # 关闭连接 + connection.close() - current_time = datetime.now() - current_date = current_time.strftime('%Y-%m-%d %H:%M:%S') + def get_messages(self, group_id, all_contacts: dict): + # 连接到数据库 + with self.db_pool.get_connection() as connection: + # 获取 redis 中的上次总结时间,本次从上次开始算,若没有,则从 8 小时之前开始计算 + key = f"{group_id}:summary_time" + last_summary_time = self._get_redis_connection().get(key) - if not last_summary_time: - # 获取当前时间并计算 8 小时前的时间 - eight_hours_ago = current_time - timedelta(hours=8) - last_summary_time = eight_hours_ago.strftime('%Y-%m-%d %H:%M:%S') - else: - # 检查 redis 中的时间与当前时间差是否小于 3 小时 - last_summary_time_obj = datetime.strptime(last_summary_time, '%Y-%m-%d %H:%M:%S') - time_diff = current_time - last_summary_time_obj + # 如果 Redis 返回值为字节类型,转换为字符串 + if last_summary_time: + last_summary_time = last_summary_time.decode('utf-8') - if time_diff < timedelta(hours=3): - # 如果小于 3 小时,取当天的内容 - last_summary_time = current_time.replace(hour=0, minute=0, second=0, microsecond=0).strftime( - '%Y-%m-%d %H:%M:%S') - elif time_diff > timedelta(days=1): - # 如果超过 24 小时,将时间设置为当天 0 点 - last_summary_time = current_time.replace(hour=0, minute=0, second=0, microsecond=0).strftime( - '%Y-%m-%d %H:%M:%S') + current_time = datetime.now() + current_date = current_time.strftime('%Y-%m-%d %H:%M:%S') - # 更新 Redis 存储的当前时间 - # r.set(key, current_date) + if not last_summary_time: + # 获取当前时间并计算 8 小时前的时间 + eight_hours_ago = current_time - timedelta(hours=8) + last_summary_time = eight_hours_ago.strftime('%Y-%m-%d %H:%M:%S') + else: + # 检查 redis 中的时间与当前时间差是否小于 3 小时 + last_summary_time_obj = datetime.strptime(last_summary_time, '%Y-%m-%d %H:%M:%S') + time_diff = current_time - last_summary_time_obj - with connection.cursor() as cursor: - # 执行查询,获取最近 8 小时的消息 - query = """ - SELECT timestamp, sender, content,message_type - FROM messages - WHERE timestamp >= %s AND message_type in(1,49) AND group_id = %s - """ - cursor.execute(query, (last_summary_time, group_id)) + if time_diff < timedelta(hours=3): + # 如果小于 3 小时,取当天的内容 + last_summary_time = current_time.replace(hour=0, minute=0, second=0, microsecond=0).strftime( + '%Y-%m-%d %H:%M:%S') + elif time_diff > timedelta(days=1): + # 如果超过 24 小时,将时间设置为当天 0 点 + last_summary_time = current_time.replace(hour=0, minute=0, second=0, microsecond=0).strftime( + '%Y-%m-%d %H:%M:%S') - # 构建最终的结果字符串 - # message_type 需要加入49类型,因为49是引用之后的发言。但是49是xml ,需要将content进行xml解析 + # 更新 Redis 存储的当前时间 + # r.set(key, current_date) - result = [] - for row in cursor.fetchall(): - timestamp, sender, content, message_type = row - try: - if message_type == "49": - # 解析 XML 字符串 - root = ET.fromstring(content) - # 提取 title 内容 - content = root.find('.//title').text + with connection.cursor() as cursor: + # 执行查询,获取最近 8 小时的消息 + query = """ + SELECT timestamp, sender, content,message_type + FROM messages + WHERE timestamp >= %s AND message_type in(1,49) AND group_id = %s + """ + cursor.execute(query, (last_summary_time, group_id)) - except Exception as e: - print(f"message_type 49 error: {e}") - sender_name = all_contacts.get(sender, sender) # 获取发送者的名字,若找不到则使用原 ID - result.append(f"{timestamp},{sender_name},{content}") + # 构建最终的结果字符串 + # message_type 需要加入49类型,因为49是引用之后的发言。但是49是xml ,需要将content进行xml解析 - result_str = "\n".join(result) # 将结果拼接为最终字符串 - print(result_str) - return result_str + result = [] + for row in cursor.fetchall(): + timestamp, sender, content, message_type = row + try: + if message_type == "49": + # 解析 XML 字符串 + root = ET.fromstring(content) + # 提取 title 内容 + content = root.find('.//title').text + except Exception as e: + print(f"message_type 49 error: {e}") + sender_name = all_contacts.get(sender, sender) # 获取发送者的名字,若找不到则使用原 ID + result.append(f"{timestamp},{sender_name},{content}") -# 示例用法 -if __name__ == "__main__": - # group_id = 'XXX@123123' - # timestamp_str = "2025-02-06 11:15:28" - # sender = "XXX" - # content = "This is a test message with a string timestamp." - # message_type = "text" - # attachment_url = "http://example.com/attachment.pdf" # 可以为None如果没有附件 - # - # archive_message(group_id, timestamp_str, sender, content, message_type, attachment_url) - # get_messages("45317011307@chatroom", {}) - message_summary(get_messages("45317011307@chatroom", {})) + result_str = "\n".join(result) # 将结果拼接为最终字符串 + print(result_str) + return result_str diff --git a/music/bot_music.py b/music/bot_music.py new file mode 100644 index 0000000..ff6c3ca --- /dev/null +++ b/music/bot_music.py @@ -0,0 +1,63 @@ +import logging +import tomllib + +import aiohttp +from wcferry import WxMsg, Wcf + +from robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager + + +class BotMusic: + def __init__(self, wcf: Wcf, gbm: GroupBotManager): + self.LOG = logging.getLogger(__name__) + self.wcf = wcf # 假设 wcf 对象在此类中初始化 + self.gbm = gbm # 权限功能 + with open("music/config.toml", "rb") as f: + plugin_config = tomllib.load(f) + + config = plugin_config["Music"] + + self.enable = config["enable"] + self.command = config["command"] + self.command_format = config["command-format"] + self.LOG.info(f"[点歌台] 组件初始化完成,指令: {self.command}") + + async def get_music(self, message: WxMsg): + if not self.enable: + return + + content = str(message.content).strip() + command = content.split(" ") + + if command[0] not in self.command: + return + + if len(command) == 1: + self.wcf.send_text(f"-----Bot-----\n❌命令格式错误!{self.command_format}", + (message.roomid if message.from_group() else message.sender), message.sender) + return + + # 如果触发了指令,但是没有权限,则返回权限不足 + if self.gbm.get_group_permission(message.roomid, Feature.TASK_GAME) == PermissionStatus.DISABLED: + return + + song_name = content[len(command[0]):].strip() + + async with aiohttp.ClientSession() as session: + async with session.get( + f"https://www.hhlqilongzhu.cn/api/dg_wyymusic.php?gm={song_name}&n=1&br=2&type=json") as resp: + data = await resp.json() + + if data["code"] != 200: + self.wcf.send_text(f"-----Bot-----\n❌点歌失败!\n{data}", + (message.roomid if message.from_group() else message.sender), message.sender) + return + title = data["title"] + singer = data["singer"] + url = data["link"] + music_url = data["music_url"].split("?")[0] + cover_url = data["cover"] + lyric = data["lrc"] + + xml = f"""{title}{singer}view30{url}{music_url}{url}{music_url}{cover_url}{lyric}000{cover_url}{bot.wxid}01""" + self.wcf.send_xml(message.sender, xml, 3) diff --git a/music/config.toml b/music/config.toml new file mode 100644 index 0000000..50adc7b --- /dev/null +++ b/music/config.toml @@ -0,0 +1,8 @@ +[Music] +enable = true +command = ["点歌", "音乐", "音乐点播", "点播音乐", "音乐点歌"] +command-format = """ +-----Bot----- +🎵点歌指令: +点歌 歌曲名 +""" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 69299e3..461bf64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,4 +26,8 @@ websocket-client~=1.8.0 selenium~=4.28.1 webdriver-manager~=4.0.2 reportlab~=4.3.0 -PyPDF2~=3.0.1 \ No newline at end of file +PyPDF2~=3.0.1 +Flask~=3.1.0 +aiohttp~=3.11.12 +mysql-connector-python~=9.2.0 +pytz~=2025.1 \ No newline at end of file diff --git a/robot.py b/robot.py index f173792..048b61b 100644 --- a/robot.py +++ b/robot.py @@ -9,6 +9,8 @@ from threading import Thread from datetime import datetime, timedelta import random +import redis + from base.func_doubao import Doubao from base.func_epic import is_friday, get_free from base.func_zhipu import ZhiPu @@ -29,16 +31,19 @@ from game_task.game_task_encyclopedia import game_process_message, setup_schedul run_random_task_assignment from group_auto.group_auto_invite import get_first_group_id, process_command from group_auto.group_member_change import GroupMemberChange +from message_sign.main import SignInSystem +from message_storage.message_to_db import MessageStorage +from music.bot_music import BotMusic from robot_cmd.robot_command import GroupBotManager from job_mgmt import Job from robot_cmd.robot_command import Feature from robot_cmd.robot_command import PermissionStatus +import mysql.connector.pooling __version__ = "39.2.4.0" from message_report.process_message import process_message from message_report.write_db import write_to_db, generate_and_send_ranking -from message_storage.message_to_db import archive_message, get_messages from message_summary.message_summary_4o import message_summary from sehuatang.shehuatang import pdf_file_path from xiuren.meitu_dl import meitu_dowload_pic, meitu_dowload_pub_pic, meitu_dowload_heisi_pic @@ -55,11 +60,27 @@ class Robot(Job): self.config = config self.LOG = logging.getLogger("Robot") self.wxid = self.wcf.get_self_wxid() - self.allContacts = self.getAllContacts() + self.allContacts = self.get_all_contacts() + self.LOG.info(f"DB+REDIS 连接池开始初始化") + # db 配置加载 + self.db_pool = mysql.connector.pooling.MySQLConnectionPool(self.config.mariadb) + self.LOG.info(f"DB连接池加载完成: {self.config.mariadb}") + self.redis_pool = redis.ConnectionPool(self.config.redis) + self.LOG.info(f"REDIS连接池加载完成: {self.config.redis}") + self.groups = {} # 存储按group_id分组的消息列表,每个group_id最多保留10条消息 GroupBotManager.load_local_cache() + # 消息存档模块初始化,自动完成入库动作 + self.message_storage = MessageStorage(self.db_pool, self.redis_pool) + # 权限模块加载 self.gbm = GroupBotManager() - self.gmc = GroupMemberChange(wcf) + # 群成员变更模块加载 + self.gmc = GroupMemberChange(wcf, self.redis_pool) + # 点歌模块加载 + self.music = BotMusic(wcf, self.gbm) + # 签到模块加载 + self.signin = SignInSystem(wcf, self.gbm, self.allContacts, self.db_pool, self.redis_pool) + if ChatType.is_in_chat_types(chat_type): if chat_type == ChatType.TIGER_BOT.value and TigerBot.value_check(self.config.TIGERBOT): self.chat = TigerBot(self.config.TIGERBOT) @@ -132,13 +153,13 @@ class Robot(Job): if cy.isChengyu(text): rsp = cy.getNext(text) if rsp: - self.sendTextMsg(rsp, msg.roomid) + self.send_text_msg(rsp, msg.roomid) status = True elif flag in ["?", "?"]: # 查词 if cy.isChengyu(text): rsp = cy.getMeaning(text) if rsp: - self.sendTextMsg(rsp, msg.roomid) + self.send_text_msg(rsp, msg.roomid) status = True return status @@ -172,15 +193,15 @@ class Robot(Job): player_id = resp["player_id"] print(f"消息: {message}") print(f"玩家ID: {player_id}") - self.sendTextMsg(message, msg.roomid, msg.sender) + self.send_text_msg(message, msg.roomid, msg.sender) except Exception as e: self.LOG.error(f"game_message_load error:{e}") return True if q == "#今日百度新闻": - self.newsBaiduReport((msg.roomid if msg.from_group() else msg.sender)) + self.news_baidu_report((msg.roomid if msg.from_group() else msg.sender)) return True elif q in ["nbc", "cnn", "abc", "fox", "bbc"]: - self.newsEnReport(q, (msg.roomid if msg.from_group() else msg.sender)) + self.news_en_report(q, (msg.roomid if msg.from_group() else msg.sender)) return True elif q == '#总结': self.message_summary_robot((msg.roomid if msg.from_group() else msg.sender)) @@ -221,7 +242,7 @@ class Robot(Job): # 如果是群消息,并且群没开启AI,则不处理该动作 if msg.from_group() and self.gbm.get_group_permission(msg.roomid, Feature.AI_CAPABILITY) == PermissionStatus.DISABLED: - self.sendTextMsg("群AI功能未开启", msg.roomid, msg.sender) + self.send_text_msg("群AI功能未开启", msg.roomid, msg.sender) return True else: if msg.type == 1: # 只处理类型为1的消息提供的问题,引用@不予以对话 @@ -230,9 +251,9 @@ class Robot(Job): return True if rsp: if msg.from_group(): - self.sendTextMsg(rsp, msg.roomid, msg.sender) + self.send_text_msg(rsp, msg.roomid, msg.sender) else: - self.sendTextMsg(rsp, msg.sender) + self.send_text_msg(rsp, msg.sender) return True else: @@ -259,7 +280,7 @@ class Robot(Job): # 聊天记录入库动作: try: now_time = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) - archive_message(msg.roomid, now_time, msg.sender, msg.content, msg.type, msg.extra) + self.message_storage.archive_message(msg.roomid, now_time, msg.sender, msg.content, msg.type, msg.extra) except Exception as e: self.LOG.error(f"archive_message error: {e}") @@ -268,7 +289,7 @@ class Robot(Job): if msg.from_self(): self.revoke_receive_message(msg) rsp = self.gbm.handle_command(msg.roomid, msg.content) - self.sendTextMsg(rsp, msg.roomid, msg.sender) + self.send_text_msg(rsp, msg.roomid, msg.sender) return except Exception as e: self.LOG.error(f"revoke_receive_message error: {e}") @@ -288,7 +309,7 @@ class Robot(Job): player_id = resp["player_id"] print(f"消息: {message}") print(f"玩家ID: {player_id}") - self.sendTextMsg(message, msg.roomid, msg.sender) + self.send_text_msg(message, msg.roomid, msg.sender) return except Exception as e: self.LOG.error(f"game_message_load error:{e}") @@ -298,25 +319,25 @@ class Robot(Job): # 判断是否没有变化 if "$NO_CHANGE$" not in result: self.LOG.info(f"检测到群成员变化,进行相关内容输出:{result}") - self.sendTextMsg(result, msg.roomid) + self.send_text_msg(result, msg.roomid) except Exception as e: self.LOG.error(f"group_member_change error: {e}") + try: + self.music.get_music(message=msg) + except Exception as e: + self.LOG.error(f"get_music error: {e}") if msg.is_at(self.wxid): # 被@ self.toAt(msg) - - else: # 其他消息 - self.toChengyu(msg) - return # 处理完群聊信息,后面就不需要处理了 # 非群聊信息,按消息类型进行处理 if msg.type == 37: # 好友请求 self.LOG.info(f"收到好友请求:{msg}") - self.autoAcceptFriendRequest(msg) + self.auto_accept_friend_request(msg) elif msg.type == 10000: # 系统信息 - self.sayHiToNewFriend(msg) + self.say_hi_to_new_friend(msg) elif msg.type == 0x01: # 文本消息 # 让配置加载更灵活,自己可以更新配置。也可以利用定时任务更新。 @@ -326,15 +347,15 @@ class Robot(Job): self.gbm.load_local_cache() self.LOG.info("已更新") if msg.content == "今日百度新闻": - self.newsBaiduReport() + self.news_baidu_report() if msg.content == '聊天排行榜': - self.generateAndSendRanking() + self.generate_and_send_ranking() if msg.content == '聊天数据入库': - self.messageCountToDB() + self.message_count_to_db() if msg.content == 'PDF': - self.generateSehuatangPdf() + self.generate_sehuatang_pdf() if msg.content == 'GROUP_LIST': - self.sendTextMsg(self.gbm.get_group_list(), msg.sender) + self.send_text_msg(self.gbm.get_group_list(), msg.sender) if msg.content.startswith('#加群配置'): # msg_content = "# 加群配置|add 原生鱼 xxx@room" parts = msg.content.split('|') @@ -344,8 +365,8 @@ class Robot(Job): resp = process_command(after_pipe) else: resp = process_command("help") - self.sendTextMsg(resp, msg.sender) - self.sendTextMsg(f"指令:{msg.content} 已执行", msg.sender) + self.send_text_msg(resp, msg.sender) + self.send_text_msg(f"指令:{msg.content} 已执行", msg.sender) else: self.toChitchat(msg) # 闲聊 @@ -376,7 +397,7 @@ class Robot(Job): self.wcf.enable_receiving_msg() Thread(target=innerProcessMsg, name="GetMessage", args=(self.wcf,), daemon=True).start() - def sendTextMsg(self, msg: str, receiver: str, at_list: str = "") -> None: + def send_text_msg(self, msg: str, receiver: str, at_list: str = "") -> None: """ 发送消息 :param msg: 消息字符串 :param receiver: 接收人wxid或者群id @@ -405,7 +426,7 @@ class Robot(Job): self.LOG.info(f"To {receiver}: {ats}\r{msg}") self.wcf.send_text(f"{ats}\n\n{msg}", receiver, at_list) - def getAllContacts(self) -> dict: + def get_all_contacts(self) -> dict: """ 获取联系人(包括好友、公众号、服务号、群成员……) 格式: {"wxid": "NickName"} @@ -413,7 +434,7 @@ class Robot(Job): contacts = self.wcf.query_sql("MicroMsg.db", "SELECT UserName, NickName FROM Contact;") return {contact["UserName"]: contact["NickName"] for contact in contacts} - def keepRunningAndBlockProcess(self) -> None: + def keep_running_and_block_process(self) -> None: """ 保持机器人运行,不让进程退出 """ @@ -421,7 +442,7 @@ class Robot(Job): self.runPendingJobs() time.sleep(1) - def autoAcceptFriendRequest(self, msg: WxMsg) -> None: + def auto_accept_friend_request(self, msg: WxMsg) -> None: try: xml = ET.fromstring(msg.content) v3 = xml.attrib["encryptusername"] @@ -432,12 +453,12 @@ class Robot(Job): except Exception as e: self.LOG.error(f"同意好友出错:{e}") - def sayHiToNewFriend(self, msg: WxMsg) -> None: + def say_hi_to_new_friend(self, msg: WxMsg) -> None: nickName = re.findall(r"你已添加了(.*),现在可以开始聊天了。", msg.content) if nickName: # 添加了好友,更新好友列表 self.allContacts[msg.sender] = nickName[0] - self.sendTextMsg(f"Hi {nickName[0]},我自动通过了你的好友请求。", msg.sender) + self.send_text_msg(f"Hi {nickName[0]},我自动通过了你的好友请求。", msg.sender) def send_group_txt_message(self, msg: str, feature: Feature): try: @@ -446,7 +467,7 @@ class Robot(Job): return for r in receivers: if self.gbm.get_group_permission(r, feature) == PermissionStatus.ENABLED: - self.sendTextMsg(msg, r) + self.send_text_msg(msg, r) except Exception as e: self.LOG.error(f"send_group_txt_message:{feature.description} error:{e}") @@ -497,28 +518,28 @@ class Robot(Job): # ============================================== 业务内容========================================================== - def newsBaiduReportAuto(self) -> None: + def news_baidu_report_auto(self) -> None: try: news = News().get_baidu_news() self.send_group_txt_message(news, Feature.DAILY_NEWS) except Exception as e: self.LOG.error(f"newsBaiduReportAuto error:{e}") - def newsBaiduReport(self, sender: str = None) -> None: + def news_baidu_report(self, sender: str = None) -> None: try: news = News().get_baidu_news() - self.sendTextMsg(news, sender) + self.send_text_msg(news, sender) except Exception as e: self.LOG.error(f"newsBaiduReport error:{e}") - def newsEnReport(self, website, sender: str = None) -> None: + def news_en_report(self, website, sender: str = None) -> None: try: news = News().get_eng_news(website) - self.sendTextMsg(news, sender) + self.send_text_msg(news, sender) except Exception as e: self.LOG.error(f"newsEnReport error:{e}") - def sendEpicFreeGames(self): + def send_epic_free_games(self): try: if is_friday(): games = get_free() @@ -526,13 +547,13 @@ class Robot(Job): except Exception as e: self.LOG.error(f"sendEpicFreeGames error:{e}") - def messageCountToDB(self): + def message_count_to_db(self): try: write_to_db() except Exception as e: self.LOG.error(f"write_to_db error:{e}") - def generateSehuatangPdf(self): + def generate_sehuatang_pdf(self): try: path = pdf_file_path() # 暂时只发4K群 @@ -540,14 +561,14 @@ class Robot(Job): except Exception as e: self.LOG.error(f"generateSehuatangPdf error:{e}") - def generateAndSendRanking(self): + def generate_and_send_ranking(self): try: receivers = self.gbm.get_group_list() if not receivers: return for r in receivers: if self.gbm.get_group_permission(r, Feature.DAILY_SUMMARY) == PermissionStatus.ENABLED: - self.sendTextMsg(generate_and_send_ranking(r, self.allContacts), r) + self.send_text_msg(generate_and_send_ranking(r, self.allContacts), r) except Exception as e: self.LOG.error(f"SendRanking error:{e}") @@ -555,11 +576,11 @@ class Robot(Job): try: if self.gbm.get_group_permission(sender, Feature.SUMMARY_CAPABILITY) == PermissionStatus.ENABLED: self.LOG.info(f"群: {sender} 消息总结开始执行!") - content = get_messages(sender, self.allContacts) + content = self.message_storage.get_messages(sender, self.allContacts) summary = message_summary(content) - self.sendTextMsg(summary, sender) + self.send_text_msg(summary, sender) else: - self.sendTextMsg("群发言总结功能未开启", sender) + self.send_text_msg("群发言总结功能未开启", sender) except Exception as e: self.LOG.error(f"message_summary_robot error:{e}") @@ -574,7 +595,7 @@ class Robot(Job): player_id = rep["player_id"] print(f"消息: {message}") print(f"玩家ID: {player_id}") - self.sendTextMsg(message, gid, player_id) + self.send_text_msg(message, gid, player_id) except Exception as e: self.LOG.error(f"message_summary_robot error:{e}") diff --git a/robot_cmd/robot_command.py b/robot_cmd/robot_command.py index b1a0c2e..fbde5aa 100644 --- a/robot_cmd/robot_command.py +++ b/robot_cmd/robot_command.py @@ -31,6 +31,7 @@ class Feature(Enum): EPIC = 7, "EPIC自动播报" # 新增的功能 PIC = 8, "图来能力" TASK_GAME = 9, "百科答题游戏" + MUSIC = 10, "点歌功能" def __new__(cls, value, description): obj = object.__new__(cls)