commit 3b191544267360da3cfbe5cb0696e74b50a28a29 Author: MelodyKnit <2711402357@qq.com> Date: Mon Dec 6 08:45:21 2021 +0800 first commit diff --git a/.env b/.env new file mode 100644 index 0000000..f772a13 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +ENVIRONMENT=dev + +SUPERUSERS=[""] +COMMAND_START=[""] +COMMAND_SEP=["."] +SESSION_EXPIRE_TIMEOUT=90 +API_TIMEOUT=300 + +MYSQL_DB = "" +MYSQL_USER = "" +MYSQL_HOST = "" +MYSQL_PASSWORD = "" diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..e0716b8 --- /dev/null +++ b/.env.prod @@ -0,0 +1,4 @@ +HOST=127.0.0.1 +PORT=8080 +SECRET= +ACCESS_TOKEN= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91e6230 --- /dev/null +++ b/.gitignore @@ -0,0 +1,146 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Data +data/class_system/* +data/chatword/* +tests + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env.dev +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ae83c30 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 + +RUN python3 -m pip config set global.index-url https://mirrors.aliyun.com/pypi/simple + +RUN python3 -m pip install poetry && poetry config virtualenvs.create false + +COPY ./pyproject.toml ./poetry.lock* /app/ + +RUN poetry install --no-root --no-dev \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8320955 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +

班级管理机器人

+ +

基于Nonebot2编写与go-cqhttp编写的班级助手

+ +
+python +nonebot +go-cqhttp +
+
+ +- 班级管理实现功能 + - 导入班级信息(只有班级教师可以导入) + - 增加院系,班级,教师(用于导入班级信息) + - 班级成员查询与at(导入班级信息后可使用以下内容) + - 签到 + - 德育分记录 + - 导出德育 + - 导出证明 +- 其它功能 + - bilibili视频解析 + - 天气查询 + - 疫情查询 + - 动漫资源获取 + - 获取ip + - 随机内容 + - 聊天 + +
+ 班级管理功能 +
\ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..4fc0e0e --- /dev/null +++ b/bot.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import nonebot +from os import getcwd +from nonebot.adapters.cqhttp import Bot + + +# Custom your logger +# +# from nonebot.log import logger, default_format +# logger.add("error.log", +# rotation="00:00", +# diagnose=False, +# level="ERROR", +# format=default_format) + +# You can pass some keyword args config to init function +nonebot.init() +app = nonebot.get_asgi() + +driver = nonebot.get_driver() +driver.register_adapter("cqhttp", Bot) + + +nonebot.load_plugins("src/utils") +nonebot.load_from_toml("pyproject.toml") +nonebot.load_plugins("src/class_management_system") + +# Modify some config / config depends on loaded configs +# +# config = driver.config +# do something... + + +if __name__ == "__main__": + nonebot.logger.warning("Always use `nb run` to start the bot instead of manually running!") + nonebot.run(app="__mp_main__:app") diff --git a/config/class_info.sql b/config/class_info.sql new file mode 100644 index 0000000..1bade87 --- /dev/null +++ b/config/class_info.sql @@ -0,0 +1,100 @@ +CREATE TABLE IF NOT EXISTS faculty_table ( + faculty VARCHAR(30) PRIMARY KEY COMMENT '院系', + invitee VARCHAR(99) NOT NULL COMMENT '添加人' +); + +CREATE TABLE IF NOT EXISTS teacher ( + name VARCHAR(20) NOT NULL COMMENT '教师姓名', + qq BIGINT PRIMARY KEY COMMENT '教师qq号', + invitee VARCHAR(99) NOT NULL COMMENT '添加人', + telephone BIGINT NOT NULL UNIQUE COMMENT '教师电话' +); + +CREATE TABLE IF NOT EXISTS expertise_table ( + faculty VARCHAR(30) NOT NULL COMMENT '院系', + expertise VARCHAR(60) PRIMARY KEY COMMENT '专业', + FOREIGN KEY (faculty) REFERENCES faculty_table(faculty) +); + +CREATE TABLE IF NOT EXISTS class_table ( + class_id BIGINT NOT NULL COMMENT '班级号', + expertise VARCHAR(60) NOT NULL COMMENT '专业', + class_group BIGINT NOT NULL UNIQUE COMMENT '班级QQ群', + class_name VARCHAR(255) PRIMARY KEY COMMENT '班级群名', + class_teacher BIGINT NOT NULL COMMENT '班主任QQ', + FOREIGN KEY (class_teacher) REFERENCES teacher(qq), + FOREIGN KEY (expertise) REFERENCES expertise_table(expertise) +); + +CREATE TABLE IF NOT EXISTS score_log ( + class_name VARCHAR(255) NOT NULL COMMENT '班级群名', + score_type VARCHAR(50) COMMENT '分数类型', + explain_reason TEXT COMMENT '解释原因原因', + name VARCHAR(20) NOT NULL COMMENT '学生姓名', + student_id BIGINT NOT NULL COMMENT '学生学号', + score INT COMMENT '加减的分数', + qq BIGINT NOT NULL COMMENT '学生qq', + log_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '日志时间', + prove VARCHAR(255) DEFAULT "" COMMENT "证明文件" +); + +CREATE TABLE IF NOT EXISTS user_info ( + 姓名 VARCHAR(20) NOT NULL COMMENT '学生姓名', + 班级 VARCHAR(255) NOT NULL COMMENT '班级名称', + 序号 INT NOT NULL COMMENT '个人在班级中的序号', + 学号 BIGINT NOT NULL COMMENT '学号', + 性别 VARCHAR(10) NOT NULL COMMENT '性别', + 联系方式 BIGINT NOT NULL COMMENT "学生联系方式", + 身份证号 VARCHAR(20) COMMENT '学生身份证', + 出生日期 TIMESTAMP COMMENT '出生日期', + 寝室 VARCHAR(10) COMMENT '寝室号', + 寝室长 VARCHAR(5) COMMENT '是否为寝室长', + 微信 VARCHAR(100) COMMENT '微信号', + QQ BIGINT PRIMARY KEY COMMENT 'QQ', + 邮箱 VARCHAR(100) COMMENT '邮箱号', + 民族 VARCHAR(200) COMMENT '民族', + 籍贯 VARCHAR(200) COMMENT '籍贯', + 职位 VARCHAR(50) NOT NULL DEFAULT '学生' COMMENT '学生', + 团员 VARCHAR(10) COMMENT '是否为团员', + 入党积极分子 VARCHAR(10) COMMENT '是否为入党积极分子', + 团学 VARCHAR(100) COMMENT '团学干部', + 社团 VARCHAR(200) COMMENT '加入的社团', + 分数 INT NOT NULL DEFAULT 0 COMMENT '分数', + FOREIGN KEY (班级) REFERENCES class_table(class_name) +); + +CREATE TABLE IF NOT EXISTS shop ( + teacher BIGINT NOT NULL COMMENT '教师QQ', + shop_name VARCHAR(255) NOT NULL COMMENT '商品', + shop_price INT NOT NULL DEFAULT 0 COMMENT '商品价格', + FOREIGN KEY (teacher) REFERENCES teacher(qq) +); + + +CREATE TABLE IF NOT EXISTS receiving ( + title VARCHAR(255) NOT NULL COMMENT '收取标题', + type VARCHAR(255) NOT NULL COMMENT '收取类型', + class_name VARCHAR(255) NOT NULL COMMENT '班级名称', + initiate BIGINT NOT NULL COMMENT '发起人', + completed boolean NOT NULL DEFAULT FALSE COMMENT '是否已经完成', + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' +); + + +CREATE TABLE IF NOT EXISTS receiving_pictures ( + title VARCHAR(255) NOT NULL COMMENT '收取标题', + user_id BIGINT NOT NULL COMMENT '提交人QQ', + user_name VARCHAR(20) NOT NULL COMMENT '提交人姓名', + class_name VARCHAR(255) NOT NULL COMMENT '班级名称', + file_name VARCHAR(255) NOT NULL COMMENT '文件名称md5', + create_time TIMESTAMP NOT NULL COMMENT '创建时间', + push_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间' +); + + +CREATE TABLE IF NOT EXISTS cost ( + thing TEXT NOT NULL COMMENT '费用所花费在某件事情', + money DOUBLE NOT NULL COMMENT '花费金额', + +) + diff --git a/data/fonts/simhei.ttf b/data/fonts/simhei.ttf new file mode 100644 index 0000000..5bd4687 Binary files /dev/null and b/data/fonts/simhei.ttf differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..27990db --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "2" +services: + # 其他配置参考 https://hub.docker.com/r/tiangolo/uvicorn-gunicorn-fastapi/ + nonebot: + build: . + volumes: + - "/etc/localtime:/etc/localtime" + - "./:/app/" + ports: + - "8080:8080" # 映射端口到宿主机 宿主机端口:容器端口 + env_file: + - ".env.prod" # fastapi 使用的环境变量文件 + environment: + - ENVIRONMENT=prod # 配置 nonebot 运行环境,此项会被 .env 文件覆盖 + - APP_MODULE=bot:app # 配置 asgi 入口 + - SECRET # 通过 SECRET=xxx nb up -d 传递密钥 + - ACCESS_TOKEN # 通过 ACCESS_TOKEN=xxx nb up -d 传递密钥 + - MAX_WORKERS=1 # 如果你有多个QQ,且存在 self_id 指定,多个 worker 会导致无法找到其他 websocket 连接 + network_mode: bridge diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..15f5adb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.poetry] +name = "classRobot" +version = "0.1.0" +description = "classRobot" +authors = [] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.7" +nonebot2 = "^2.0.0.a1" +nb-cli = "^0.1.0" + +[tool.poetry.dev-dependencies] +nonebot-test = "^0.1.0" + +[nonebot.plugins] +plugins = [] +plugin_dirs = ["src/plugins"] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ddf1f7 Binary files /dev/null and b/requirements.txt differ diff --git a/src/class_management_system/add_friends/__init__.py b/src/class_management_system/add_friends/__init__.py new file mode 100644 index 0000000..8ce8215 --- /dev/null +++ b/src/class_management_system/add_friends/__init__.py @@ -0,0 +1,13 @@ +from nonebot import on_request, require +from nonebot.adapters.cqhttp import Bot, FriendRequestEvent + + +friends = on_request() +IS_USER = require("rule").IS_USER + + +@friends.handle() +async def friends_handle(bot: Bot, event: FriendRequestEvent, state: dict): + if await IS_USER(bot, event, state): + text = f"{state['user_info']['班级']}-{state['user_info']['姓名']}" + await bot.set_friend_add_request(flag=event.flag, approve=True, remark=text) diff --git a/src/class_management_system/birthday/README.md b/src/class_management_system/birthday/README.md new file mode 100644 index 0000000..9370a05 --- /dev/null +++ b/src/class_management_system/birthday/README.md @@ -0,0 +1,3 @@ +# 生日插件 + +每天检查是否有用户生日,会在用户所在班级群内发送生日消息 \ No newline at end of file diff --git a/src/class_management_system/birthday/__init__.py b/src/class_management_system/birthday/__init__.py new file mode 100644 index 0000000..0ae367c --- /dev/null +++ b/src/class_management_system/birthday/__init__.py @@ -0,0 +1,21 @@ +from pprint import pprint + +from nonebot import require, get_driver, get_bot +from asyncio import sleep +from nonebot.adapters.cqhttp import Bot, Message +from .data_source import config, SelectBirthdayUser + +driver = get_driver() +scheduler = require("nonebot_plugin_apscheduler").scheduler + + +async def birthday(bot: Bot): + async with SelectBirthdayUser() as res: + for txt, group_id in res.text(): + await sleep(5) + await bot.send_group_msg(group_id=group_id, message=txt) + + +@driver.on_bot_connect +async def _(bot: Bot): + scheduler.add_job(birthday, 'cron', hour=0, minute=0, id='birthday', args=[bot]) diff --git a/src/class_management_system/birthday/config.py b/src/class_management_system/birthday/config.py new file mode 100644 index 0000000..bf19362 --- /dev/null +++ b/src/class_management_system/birthday/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + user_table = "user_info" + class_table = "class_table" + notification_time = 0 diff --git a/src/class_management_system/birthday/data_source.py b/src/class_management_system/birthday/data_source.py new file mode 100644 index 0000000..6c85755 --- /dev/null +++ b/src/class_management_system/birthday/data_source.py @@ -0,0 +1,44 @@ +from datetime import datetime +from nonebot import require +from nonebot.adapters.cqhttp import Message, MessageSegment +from pandas import DataFrame +from .config import Config + +mysql = require("botdb").MySQLdbMethods() +config = Config() + + +class SelectBirthdayUser: + def __init__(self): + self.now = datetime.now() + self.month = self.now.month + self.day = self.now.day + self.class_user = dict() + self.reply = "" + + async def birthday_user(self) -> DataFrame: + """获取生日的用户""" + await mysql.execute(f"select 姓名,qq,班级,出生日期,class_group 群号 from {config.user_table},{config.class_table} " + f"where month(出生日期)=%s and day(出生日期)=%s and class_name=班级", + [self.month, self.day]) + return mysql.form() + + async def __aenter__(self): + self.all_user = await self.birthday_user() + self.all_class = set(self.all_user["群号"]) + for i in self.all_class: + self.class_user[i] = self.all_user[self.all_user["群号"] == i].reset_index().drop("index", axis=1) + return self + + def text(self): + for group_id in self.class_user: + for txt in [ + "咚咚咚,还有人嘛 (′▽`〃)。", + f"今天是{self.now.date()},然后呢人家想悄悄告你们一个小秘密 (/▽\)", + f"今天是”{','.join(list(self.class_user[group_id]['姓名']))}“" + f"同学的生日噢 (✧◡✧)!\n\n好啦好啦!你知道的太多了啦,接下来该做什么人家也不用多说了吧。 (>▽<) 嘻嘻~" + ]: + yield txt, group_id + + async def __aexit__(self, *args): + ... diff --git a/src/class_management_system/check_in_system/__init__.py b/src/class_management_system/check_in_system/__init__.py new file mode 100644 index 0000000..abd8077 --- /dev/null +++ b/src/class_management_system/check_in_system/__init__.py @@ -0,0 +1,101 @@ +from nonebot import on_command, require +from nonebot.rule import T_State +from nonebot.adapters.cqhttp import Bot, MessageEvent, Message, GroupMessageEvent, PrivateMessageEvent +from .data_source import SetSignIn, SignIn, QuerySignIn + +rule = require("rule") +CLASS_CADRE = rule.CLASS_CADRE +IS_USER = rule.IS_USER + +query_sign_in = on_command("签到情况", aliases={"签到状态"}) +set_login_in = on_command("添加签到", aliases={"设置签到"}) +set_group_login = on_command("添加群签到") +set_private_login = on_command("添加个人签到", aliases={"添加单独签到"}) +login_in = on_command("签到") + + +# ---------- 发起签到 ---------- +async def set_login_in_handle(bot: Bot, event: MessageEvent, state: T_State): + if await CLASS_CADRE(bot, event, state): + state["param"] = state["class_cadre"] + key = event.message + if key: + state["key"] = key + else: + await set_login_in.finish() + + +def set_login_in_got(typeof: str): + """添加签到""" + async def _(bot: Bot, event: MessageEvent, state: T_State): + state["key"] = Message(state["key"]) + async with SetSignIn(state["param"]["班级"], + state["key"], typeof) as res: + if not res.existence: + await set_login_in.finish(res.text()) + else: + await set_login_in.send(res.text()) + return _ + + +def set_login_in_reset(typeof: str): + """判断是否重置签到""" + async def _(bot: Bot, event: MessageEvent, state: T_State): + if "是" in state["existence"]: + async with SetSignIn(state["param"]["班级"], state["key"], typeof) as res: + res.reset_sign_in() + await set_login_in.finish(res.text()) + return _ + + +set_login_in.handle()(set_login_in_handle) +set_private_login.handle()(set_login_in_handle) +set_group_login.handle()(set_login_in_handle) + +set_login_in.got("key", prompt="请输入签到关键字")(set_login_in_got("any")) +set_private_login.got("key", prompt="请输入签到关键字")(set_login_in_got("private")) +set_group_login.got("key", prompt="请输入签到关键字")(set_login_in_got("group")) + +set_login_in.got("existence")(set_login_in_reset("any")) +set_private_login.got("existence")(set_login_in_reset("private")) +set_group_login.got("existence")(set_login_in_reset("group")) + + +# ---------- 签到 ---------- +@login_in.handle() +async def login_in_handle(bot: Bot, event: MessageEvent, state: T_State): + if await IS_USER(bot, event, state): + if event.message: + state["key"] = event.message + else: + await login_in.finish() + + +@login_in.got("key", prompt="请输入签到关键字") +async def login_in_got(bot: Bot, event: MessageEvent, state: T_State): + state["key"] = Message(state["key"]) + with SignIn( + event.user_id, + state["user_info"]["姓名"], + state["user_info"]["班级"], + state["key"], + event.message_type) as res: + await login_in.finish(res.text()) + + +# ---------- 查询签到 ---------- +@query_sign_in.handle() +async def query_sign_in_handle(bot: Bot, event: MessageEvent, state: T_State): + if await CLASS_CADRE(bot, event, state): + state["param"] = state["class_cadre"] + else: + await query_sign_in.finish() + + +@query_sign_in.got("param") +async def query_sign_in_got(bot: Bot, event: MessageEvent, state: T_State): + with QuerySignIn(state["param"]["班级"]) as res: + for msg in res.text(): + await query_sign_in.send(msg) + + diff --git a/src/class_management_system/check_in_system/config.py b/src/class_management_system/check_in_system/config.py new file mode 100644 index 0000000..68a5dce --- /dev/null +++ b/src/class_management_system/check_in_system/config.py @@ -0,0 +1,5 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + user_table = "user_info" diff --git a/src/class_management_system/check_in_system/data_source.py b/src/class_management_system/check_in_system/data_source.py new file mode 100644 index 0000000..d79a4c5 --- /dev/null +++ b/src/class_management_system/check_in_system/data_source.py @@ -0,0 +1,142 @@ +from nonebot import require +from typing import Dict + +from .config import Config +from nonebot.adapters.cqhttp import Message, MessageSegment +from pandas import DataFrame, concat, Series + +mysql = require("botdb").MySQLdbMethods() +config = Config() +sign_ins = {} + + +def reset_index(df: DataFrame) -> DataFrame: + return df.reset_index().drop("index", axis=1) + + +class SetSignIn: + def __init__(self, class_name: str, key: Message, typeof: str): + self.class_name = class_name # 班级名称 + self.key = key # 签到关键字 + self.sign_users = [] # 已签到用户 + self.existence = False + self.reply = "" + self.typeof = typeof + + def add_sign_in(self): + """ + 添加签到 + 判断班级是否有加入签到 + """ + if self.class_name not in sign_ins.keys() or self.existence: + self.existence = False + sign_ins[self.class_name] = self + self.reply = f"”{self.class_name}“班级成功设置班级签到,班级成员共{self.users.shape[0]}人" + else: + # 签到已经存在 + self.reply = Message(f"{self.class_name}已添加签到,是否重置签到(是/否)!!") + self.existence = True + + def reset_sign_in(self): + """重新设置签到""" + self.add_sign_in() + self.reply = f"成功重置”{self.class_name}“签到。" + + def del_sign_in(self): + """删除签到""" + sign_ins.pop(self.class_name) + + async def __aenter__(self): + """寻找出班级所有成员""" + await mysql.execute(f"select 姓名, qq from {config.user_table} where 班级=%s", [self.class_name]) + self.users: DataFrame = mysql.form() + self.add_sign_in() + return self + + def text(self) -> Message: + return Message(self.reply) + + async def __aexit__(self, *args): + ... + + +class SignIn: + def __init__(self, + user_id: int, + user_name: str, + class_name: str, + key: str, + typeof: str): + self.class_name = class_name + self.user_name = user_name + self.user_id = user_id + self.reply = "" + self.key = key + self.typeof = typeof + + def is_key(self): + return self.sign_class.key == self.key + + def __enter__(self): + # 签到的班级 + self.sign_class: SetSignIn = sign_ins.get(self.class_name) + if self.sign_class: + if self.sign_class.typeof == "any" or self.sign_class.typeof == self.typeof: + if self.is_key(): + self.start_sign() + else: + self.reply = "关键字输入错误!!!" + else: + self.reply = "签到失败,您不该在这里签到!" + else: + self.reply = "您的班级未发起签到!!!" + return self + + def start_sign(self): + """ + 开始签到 + 查询该用户在班级群内(sign_class.users)并且用户不在已到内(sign_users) + 不在sign_users就进行添加,作为签到成功 + """ + user = (reset_index(self.sign_class.users[self.sign_class.users.qq == self.user_id]).loc[0]) + if user.shape[0] and self.user_id not in self.sign_class.sign_users: + self.sign_class.sign_users.append(self.user_id) + self.reply = f"”{user['姓名']}“签到成功" + else: + self.reply = "您已经签到过了!" + + def text(self): + return Message(self.reply) + + def __exit__(self, *args): + ... + + +class QuerySignIn: + def __init__(self, class_name: str): + self.class_name = class_name + self.not_sign_user: DataFrame = DataFrame() + self.reply = "" + + def __enter__(self): + self.sign_class: SetSignIn = sign_ins.get(self.class_name) + if self.sign_class: + # 已签到用户 + self.sign_user = (self.sign_class.users[self.sign_class.users["qq"].isin(self.sign_class.sign_users)] + .reset_index().drop("index", axis=1)) + # 未签到用户 + self.not_sign_user = reset_index(concat([self.sign_class.users, self.sign_user]) + .drop_duplicates(keep=False)) + else: + self.reply = f"{self.class_name}并未发起签到!!!" + return self + + def text(self): + if self.sign_class: + yield Message("已签到\n" + " ".join(list(self.sign_user["姓名"]))) + yield Message("未签到\n" + " ".join(list(self.not_sign_user["姓名"]))) + else: + yield Message("未发起签到!!!") + + def __exit__(self, *args): + ... diff --git a/src/class_management_system/file_task/README.md b/src/class_management_system/file_task/README.md new file mode 100644 index 0000000..e14c7f7 --- /dev/null +++ b/src/class_management_system/file_task/README.md @@ -0,0 +1,30 @@ +# 任务功能(文件收取) + +## 命令 + +### 添加任务 +- 参数:任务名称 +- 说明:在你添加任务后会在这个班级内出现该任务,当班级内有任务时可以开始提交\查询\导出\删除任务 + +### 查询任务 +- 参数:任务序号(可选) +- 说明: + - 不添加参数:列出所有任务 + - 添加参数:查询任务后当添加序号会列出该任务已经提交的用户与未提交的用户,消息分两次发送 + +### 提交任务 +- 参数1:任务序号 +- 参数2:图片 +- 说明:提交的文件会与所有任务的文件进行检查,避免利用别人文件去提交 + +### 导出任务 +- 参数:任务序号 +- 说明:导出任务只可以在群内导出 + +### 删除任务 +- 参数1:任务序号 +- 参数2:确认/不删除 +- 说明: + - 删除任务后,任务提交的用户与文件会一并删除(不可恢复) + - 当任务被删除后,提交文件的检查重复文件会减少 + diff --git a/src/class_management_system/file_task/__init__.py b/src/class_management_system/file_task/__init__.py new file mode 100644 index 0000000..cf80faf --- /dev/null +++ b/src/class_management_system/file_task/__init__.py @@ -0,0 +1,160 @@ +from nonebot import on_command, require +from nonebot.adapters.cqhttp import Bot, GroupMessageEvent, MessageEvent +from .data_source import SelectTasks, AddTask, PushTaskFile, Image, ExportTask, DeleteTask + +rule = require("rule") +IS_USER = rule.IS_USER +CLASS_CADRE = rule.CLASS_CADRE +query_task = on_command("查询任务") +add_task = on_command("添加任务") +push_task_file = on_command("提交任务") +export_task = on_command("导出任务") +delete_task = on_command("删除任务") + + +# 查询任务 +@query_task.handle() +async def _(bot: Bot, event: MessageEvent, state: dict): + if await IS_USER(bot, event, state): + text = event.get_plaintext() + task: SelectTasks = await SelectTasks( + state["user_info"]["班级"], + int(text) - 1 if text.isdigit() else None) + async for msg in task.text(): + await query_task.send(msg) + + +# 添加任务 +@add_task.handle() +async def _(bot: Bot, event: MessageEvent, state: dict): + if await CLASS_CADRE(bot, event, state): + text = event.get_plaintext() + if text: + state["title"] = text + + +@add_task.got("title", prompt="这个任务的标题叫什么呢?") +async def _(bot: Bot, event: MessageEvent, state: dict): + text = event.get_plaintext() + if text: + async with AddTask(text, event.user_id, state["class_cadre"]["班级"], state["class_cadre"]["职位"]) as text: + await add_task.finish(text) + else: + await add_task.finish("不告诉我就不给你添加了,哼~") + + +# 提交任务 +@push_task_file.handle() +async def _(bot: Bot, event: MessageEvent, state: dict): + if await IS_USER(bot, event, state): + text = event.get_plaintext() + task: PushTaskFile = await PushTaskFile(state["user_info"]["班级"], int(text) - 1 if text.isdigit() else None) + state["task"] = task + if not task: + await push_task_file.finish(await task.text_one()) + elif task.index is None: + await push_task_file.send(await task.text_one()) + else: + state['index'] = task.index + + +@push_task_file.got("index", prompt="你想交第几个任务呀!") +async def _(bot: Bot, event: MessageEvent, state: dict): + index = state["index"] + task: PushTaskFile = state["task"] + if not isinstance(index, int): + text = event.get_plaintext() + index = int(text) - 1 if text.isdigit() else None + if index is None: + await push_task_file.finish(f"不提交就算了,哼~") + elif index not in task: + await push_task_file.finish(f"你有看到我给你列出{index+1}号吗?哼~") + + task(state["user_info"]["姓名"], event.user_id, index) + + +@push_task_file.got('file', prompt="文件给我吧。") +async def _(bot: Bot, event: MessageEvent, state: dict): + task: PushTaskFile = state["task"] + image = [Image(msg.data) for msg in event.message if msg.type == "image"] + if image: + image = image[0] + if await task.file_exists(image): + await push_task_file.finish("你这是拿别人的文件吧,不要瞎搞哦!") + else: + await push_task_file.finish(await task.save(image)) + else: + await push_task_file.finish("文件呢?") + + +# 导出任务 +@export_task.handle() +async def _(bot: Bot, event: GroupMessageEvent, state: dict): + if await CLASS_CADRE(bot, event, state): + text = event.get_plaintext() + task: ExportTask = await ExportTask(state["class_cadre"]["班级"], int(text) - 1 if text.isdigit() else None) + state["task"] = task + if not task: + await export_task.finish(await task.text_one()) + elif task.index is None: + await export_task.send(await task.text_one()) + else: + state['index'] = task.index + + +@export_task.got("index", prompt="你想导出第几个任务呀!") +async def _(bot: Bot, event: GroupMessageEvent, state: dict): + index = state["index"] + task: ExportTask = state["task"] + if not isinstance(index, int): + text = event.get_plaintext() + index = int(text) - 1 if text.isdigit() else None + if index is None: + await export_task.finish(f"不提交就算了,哼~") + elif index not in task: + await export_task.finish(f"你有看到我给你列出{index+1}号吗?哼~") + + if task(index).task_user.shape[0]: + await export_task.send("开始进行打包咯...") + file, name = task.zip() + await export_task.send("打包完成,开始发送文件...") + await bot.call_api("upload_group_file", group_id=event.group_id, file=file, name=name) + else: + await export_task.finish("都没人提交,你这叫我怎么导出嘛!") + + +# 删除任务 +@delete_task.handle() +async def _(bot: Bot, event: GroupMessageEvent, state: dict): + if await CLASS_CADRE(bot, event, state): + text = event.get_plaintext() + task: DeleteTask = await DeleteTask(state["class_cadre"]["班级"], int(text) - 1 if text.isdigit() else None) + state["task"] = task + if not task: + await delete_task.finish(await task.text_one()) + elif task.index is None: + await delete_task.send(await task.text_one()) + else: + state['index'] = task.index + + +@delete_task.got("index", prompt="你想删除第几个任务呀!") +async def _(bot: Bot, event: MessageEvent, state: dict): + index = state["index"] + task: ExportTask = state["task"] + if not isinstance(index, int): + text = event.get_plaintext() + index = int(text) - 1 if text.isdigit() else None + if index is None: + await delete_task.finish(f"取消删除") + elif index not in task: + await delete_task.finish(f"你有看到我给你列出{index + 1}号吗?哼~") + state["index"] = index + await delete_task.send(f"如果真的要删除”{task[index]['title']}“请发送”确认“。") + + +@delete_task.got("delete") +async def _(bot: Bot, event: MessageEvent, state: dict): + if "确认" in event.get_plaintext(): + task: DeleteTask = state["task"](state["index"]) + await delete_task.finish(await task.delete()) diff --git a/src/class_management_system/file_task/data_source.py b/src/class_management_system/file_task/data_source.py new file mode 100644 index 0000000..3852ca4 --- /dev/null +++ b/src/class_management_system/file_task/data_source.py @@ -0,0 +1,307 @@ +from typing import Union, List, Tuple, Iterator, Any +from zipfile import ZipFile, ZIP_DEFLATED + +from aiohttp import ClientSession, ClientTimeout +from nonebot import require +from pandas import DataFrame, Series +from os import path + +task = "receiving" +task_file = "receiving_pictures" +mysql = require("botdb").MySQLdbMethods() +readfile = require("readfile").ReadFile("data", "class_system") + + +class Image: + def __init__(self, data): + self.name = data.get("file").split(".")[0] + self.url = data.get("url") + + +class SaveImage: + def __init__(self, class_name: str, file_name: str = None): + self.class_name = class_name + self.image_path = path.join(class_name, "images", "tasks_file") + self.file_name = file_name + + def join(self, img: Union[Image, str]) -> str: + return path.join(readfile.path, self.image_path, f"{img if isinstance(img, str) else img.name}.jpg") + + def for_in_name(self, images: List[Union[Image, str]]) -> Iterator[str]: + for img in images: + yield self.join(img) + + async def save(self, images: List[Image]): + for img in images: + async with self.session.get(img.url) as res: + await readfile.write( + path.join(self.image_path, f"{self.file_name or img.name}.jpg"), + await res.read(), mode="wb") + + def remove(self, image: Union[str, Image]): + readfile.remove( + self.image_path, + f"{image if isinstance(image, str) else image.name}.jpg" + ) + + async def __aenter__(self): + self.session = ClientSession(timeout=ClientTimeout(total=10 * 60)) + return self + + async def __aexit__(self, *args): + await self.session.close() + + +# 查询任务 +class SelectTasks: + def __init__(self, class_name: str, index: int = None): + self.class_name = class_name + self.index = index + self.index_err = False + + def __await__(self): + return self._init().__await__() + + async def get_tasks(self) -> DataFrame: + """获取所有任务""" + await mysql.execute(f"select * from {task} where class_name=%s", [self.class_name]) + return mysql.form() + + async def select_user_in_task(self, series: Union[Series, int]) -> DataFrame: + """选择某项任务,列出所有提交任务的用户""" + if isinstance(series, int): + series = self[series] + await mysql.execute(f"select * from {task_file} where create_time=%s and class_name=%s and title=%s", + [str(series["create_time"]), self.class_name, series["title"]]) + return mysql.form() + + async def select_user_not_in_task(self, users: Union[list, set]) -> DataFrame: + await mysql.execute(f"select * from user_info where 班级=%s and qq not in({','.join(['%s'] * len(users))})", + [self.class_name, *users]) + return mysql.form() + + async def set_task(self, index: int = None) -> List[DataFrame]: + """设置task的已经提交人数,并且返回索引值对应的用户,如果索引值为空则返回所有,超出则不返回""" + task_user: List[DataFrame] = [await self.select_user_in_task(series) for i, series in self + if i == index or index is None] + + if index is None: + self.tasks['completed'] = [f"(提交{i.shape[0]}人)" for i in task_user] + elif index in self: + self.tasks.loc[index, 'completed'] = f"(提交{task_user[0].shape[0]}人)" + else: + self.index_err = True + return task_user + + async def _init(self): + self.tasks: DataFrame = await self.get_tasks() + self.length = self.tasks.shape[0] + self.task_user = await self.set_task(self.index) + return self + + @staticmethod + def submitted(user: DataFrame) -> str: + if user.shape[0]: + return "已提交\n" + ("\n".join([f"{v['user_name']} | {str(v['push_time']).split('.')[0]}" + for i, v in user.iterrows()])) + else: + return "咦,文件呢?怎么还没人提交呀!" + + @staticmethod + def not_submitted(user: DataFrame) -> str: + if user.shape[0]: + return "未提交\n" + (" ".join(user['姓名'])) + return "咦,所有人都交了哎!" + + async def text_one(self): + """只回复一条消息""" + return await self.text().__anext__() + + async def text(self): + if self.index_err: + yield f"咦,我怎么不知道有{self.index + 1}号???" + else: + if self.length: + if self.index is None: + yield "\n".join( + [f"{i + 1}.{v['title']}{v['completed']} \n " + f"发起时间:{v['create_time'].date()}" for i, v in self]) + else: + yield self.submitted(self.task_user[0]) + user = await self.select_user_not_in_task(list(self.task_user[0]["user_id"])) + yield self.not_submitted(user) + else: + yield "干什么呀!你们班还没添加任务呢!" + + def __str__(self): + return self.text() + + def __iter__(self) -> Iterator[Tuple[Any, Series]]: + return iter(self.tasks.iterrows()) + + def __getitem__(self, item: int) -> Union[Series, None]: + if 0 <= item < self.length: + return self.tasks.loc[item] + return None + + def __contains__(self, index: int): + return index is not None and 0 <= index < self.length + + def __bool__(self): + return not (not self.length or self.index_err) + + +# 添加任务 +class AddTask(SelectTasks): + def __init__(self, title: str, user_id: int, class_name: str, jop: str): + super().__init__(class_name) + self.jop = jop # 发起人所担任职务 + self.title = title # 发起的标题 + self.user_id = user_id # 发起人的id + self.class_name = class_name # 发起的班级 + + async def repeat_task(self, tasks: DataFrame = None): + """ + 查询是否为重复任务 + 重复任务返回False + 不是重复任务返回True + """ + if not tasks: + tasks = await self.get_tasks() + return self.title not in list(tasks["title"]) + + async def add_task(self): + """创建一个文件收取的任务""" + await mysql.execute_commit(f"insert into {task} (title,initiate,type,class_name) value (%s,%s,%s,%s)", + [self.title, self.user_id, "image", self.class_name]) + + async def __aenter__(self): + if await self.repeat_task(): + await self.add_task() + return f"“{self.title}”创建成功 ^_^" + return "歪,你有个还有一个一模一样的任务呢!能不能先删掉那个重复的 >_ Union[bool, str]: + """用户是否提交过""" + task_user = self.task_user[0] if len(self.task_user) == 1 else self.task_user[self.index] + user_id = list(task_user["user_id"]) + if self.user_id in user_id: + return task_user["file_name"][user_id.index(self.user_id)] + return False + + @staticmethod + async def file_exists(image: Image) -> bool: + """文件是否已存在""" + await mysql.execute(f"select file_name from {task_file} where file_name=%s", [image.name]) + return bool(list(mysql.form()["file_name"])) + + async def insert(self, file_name: str, index: int = None): + _task = self[index or self.index] + await mysql.execute_commit(f"insert into {task_file} (title,user_id,user_name,class_name,file_name,create_time)" + "value (%s,%s,%s,%s,%s,%s)", + [_task['title'], self.user_id, self.user_name, + self.class_name, file_name, str(_task['create_time']).split(".")[0]]) + + async def delete(self, index: int = None): + index = index or self.index + await mysql.execute_commit(f"delete from {task_file} where class_name=%s and title=%s and user_id=%s", + [self.class_name, self[index]['title'], self.user_id]) + + async def save(self, image: Image): + async with SaveImage(self.class_name) as res: + file = self.is_exists() + if file: + res.remove(file) + await self.delete() + await res.save([image]) + await self.insert(image.name) + return "提交成功! ^_^" + + +# 导出任务 +class ExportTask(SelectTasks): + def __call__(self, index: int): + """ + :参数 index: + 任务序号 + :返回: ExportTask + """ + self.index = index + self.task_user = self.task_user[0] if len(self.task_user) == 1 else self.task_user[index] + return self + + def zip(self) -> Tuple[str, str]: + """ + :说明: + 将文件打包成压缩文件保存到本地 + :返回: + 文件路径 文件名称 + """ + file = SaveImage(self.class_name) + file_name = f"{self.class_name}{self[self.index]['title']}.zip" + file_path = path.join(readfile.path, self.class_name, file_name) + with ZipFile(file_path, "w", ZIP_DEFLATED) as zip_file: + for i, img in self.task_user.iterrows(): + # 文件名以用户名称+qq组成 + zip_file.write(file.join(img["file_name"]), f"{img['user_name']}{img['user_id']}.jpg") + return file_path, file_name + + +# 删除任务 +class DeleteTask(SelectTasks): + def __call__(self, index: int): + """ + :参数 index: + 任务序号 + :返回: DeleteTask + """ + self.index = index + self.task_user = self.task_user[0] if len(self.task_user) == 1 else self.task_user[index] + return self + + async def delete_table(self) -> str: + """ + :说明: + 删除数据表,先删除文件表在删除任务表 + :返回: + 表格标题 + """ + title = self[self.index]["title"] + await mysql.execute_commit(f"delete from {task_file} where class_name=%s and title=%s", + [self.class_name, title]) + await mysql.execute_commit(f"delete from {task} where class_name=%s and title=%s", + [self.class_name, title]) + return title + + def delete_file(self): + """ + :说明: + 删除任务文件 + """ + file = SaveImage(self.class_name) + for i, img in self.task_user.iterrows(): + file.remove(img["file_name"]) + + async def delete(self): + """ + :说明: + 先删除文件然后删除表格 + :返回: + 回复消息 + """ + self.delete_file() + title = await self.delete_table() + return f"“{title}”删除成功!" + diff --git a/src/class_management_system/find_user/__init__.py b/src/class_management_system/find_user/__init__.py new file mode 100644 index 0000000..c903643 --- /dev/null +++ b/src/class_management_system/find_user/__init__.py @@ -0,0 +1,28 @@ +from nonebot import on_command, require +from nonebot.adapters.cqhttp import Bot, GroupMessageEvent +from .query import QueryUsers + +find = on_command("find") +rule = require("rule") +IS_USER = rule.IS_USER +CLASS_GROUP = rule.CLASS_GROUP +MASTER = rule.MASTER + + +@find.handle() +async def _(bot: Bot, event: GroupMessageEvent, state: dict): + if await CLASS_GROUP(bot, event, state): + search = event.get_plaintext().split("-") + + # 判断第0个是否有输入需要搜索的某位用户,如果没有就默认为自己 + if not search[0]: + user_split = QueryUsers.split() + user_split.user_id.append(event.user_id) + else: + user_split = QueryUsers.split(event.message) + + # 判断是否存在第二个值,作为搜索展示出的内容 + search = (search[1].split() if search[-1] else None) if len(search) > 1 else None + + users = QueryUsers(state["class_group"]["class_name"], search) + await find.finish(await users.text(user_split)) diff --git a/src/class_management_system/find_user/query.py b/src/class_management_system/find_user/query.py new file mode 100644 index 0000000..5a0c158 --- /dev/null +++ b/src/class_management_system/find_user/query.py @@ -0,0 +1,151 @@ +from nonebot import require +from nonebot.adapters.cqhttp import Message +from pandas import DataFrame, Series + +mysql = require("botdb").MySQLdbMethods() + + +def get(data: Series): + def _(key: str, fmt: str = None, is_str: str = "") -> str: + """ + :param key: 关键字 + :param fmt: 替换字符 + :param is_str: 满足条件 + :return: + """ + value = data.get(key) + if value == is_str or " " != value != "None" and value: + return fmt if fmt else f"{key}:{value}" + return "" + return _ + + +def dataMap(data: DataFrame): + def _(key, arg, na_action=None): + values: Series = data.get(key) + if values is not None: + data[key] = values.map(arg, na_action) + return _ + + +class SplitUser: + def __init__(self, message: Message = None): + self.message = message + self.index = [] + self.user_id = [] + self.name = [] + if message: + for msg in message: + if msg.type == "at": + self.user_id.append(msg.data.get("qq")) + elif msg.type == "text": + for text in msg.data.get("text").split(): + if text.isdigit(): + if len(text) < 5: + self.index.append(int(text)) + else: + self.user_id.append(int(text)) + else: + self.name.append(text) + + def __bool__(self) -> bool: + return bool(self.user_id or self.index or self.name) + + +class QueryUsers: + exhibition = { + "序号", "姓名", "班级", "学号", "性别", "联系方式", "出生日期", "寝室", "寝室长", "微信", "qq", + "邮箱", "民族", "职位", "团员", "入党积极份子", "团学", "社团", "分数" + } + + def __init__(self, class_name: str, screen: set = None): + screen = screen or self.exhibition + self.class_name = class_name + self.screen = {"姓名", *screen} & self.exhibition + + def single_reply(self, users: Series) -> str: + """单条回复信息""" + find = get(users) + return "\n".join([i for i in [find(v) for v in self.screen] if i]) + + def multiple_replies(self, users: DataFrame) -> str: + """多条回复信息""" + return "\n- - - - -\n".join([self.single_reply(v) for i, v in users.iterrows()]) + + async def teacher_reply(self, users: SplitUser) -> str: + """查找到的教师回复词""" + user = await self.query_teacher(users) + length = user.shape[0] + if length: + return "\n- - - - -\n".join([f"姓名:{u['name']}\nQQ:{u['qq']}\n联系方式:{u['telephone']}" + for i, u in user.iterrows()]) + else: + return "" + + async def users_reply(self, users: SplitUser) -> str: + """查找到的学生回复词""" + user = await self.query_user(users) + length = user.shape[0] + if length == 1: + return self.single_reply(user.loc[0]) + elif length > 1: + return self.multiple_replies(user) + else: + return "" + + async def text(self, users: SplitUser = None): + text = await self.users_reply(users) + if text: + return Message(text) + text = await self.teacher_reply(users) + if text: + return Message("查询到为教师:\n- - - - -\n" + text) + return Message("未查找到该用户!!") + + async def query_user(self, users: SplitUser) -> DataFrame: + """查找班级某些用户""" + await mysql.execute(f"""select {','.join(self.screen)} from user_info where 班级=%s and ({self.set_where( + qq=users.user_id, + 学号=users.user_id, + 序号=users.index, + 姓名=users.name + )})""", [self.class_name, *(users.user_id * 2), *users.index, *users.name]) + data: DataFrame = mysql.form() + _map = dataMap(data) + _map("出生日期", lambda x: str(x).split()[0]) + _map("寝室长", lambda x: "寝室长" if x == "是" else None) + return data + + @classmethod + async def query_teacher(cls, users: SplitUser) -> DataFrame: + """查找教师""" + await mysql.execute(f"""select * from teacher where {cls.set_where( + qq=users.user_id, + name=users.name, + )}""", [*users.user_id, *users.name]) + return mysql.form() + + @staticmethod + def in_method(tag: str, al: list): + """sql的in方法""" + if al: + return f"{tag} in({','.join(['%s'] * len(al))})" + + @classmethod + def set_where(cls, format_str: str = "or", **kwargs: list): + """ + 设置where条件 + :param format_str: 需要格式化的字符 + :param kwargs: 列名与包含的数据 + :return: + 如果有数据,例如 qq=[123456] + 返回 qq in(%s) + 如果有多个条件,例如 qq=[123456], name=[654897] + 返回 qq in(%s) or name in(%s) + """ + return f" {format_str} ".join([cls.in_method(i, kwargs[i]) for i in kwargs if kwargs[i]]) + + @staticmethod + def split(message: Message = None) -> SplitUser: + """拆分出用户,索引,号码""" + return SplitUser(message) diff --git a/src/class_management_system/image_watermark/__init__.py b/src/class_management_system/image_watermark/__init__.py new file mode 100644 index 0000000..24d95b0 --- /dev/null +++ b/src/class_management_system/image_watermark/__init__.py @@ -0,0 +1,24 @@ +from nonebot import require, on_command +from nonebot.adapters.cqhttp import Bot, MessageEvent +from .data_source import SetWatermark + + +watermark = on_command("水印", aliases={"打水印", "添加水印"}) +IS_USER = require("rule").IS_USER + + +@watermark.handle() +async def watermark_handler(bot: Bot, event: MessageEvent, state: dict): + if await IS_USER(bot, event, state): + for msg in event.message: + if msg.type == "image": + state['params'] = True + break + + +@watermark.got("params", prompt="请发送图片") +async def watermark_got(bot: Bot, event: MessageEvent, state: dict): + for msg in event.message: + if msg.type == "image": + async with SetWatermark(msg.data["url"], state["user_info"]) as res: + await watermark.send(res.image()) diff --git a/src/class_management_system/image_watermark/data_source.py b/src/class_management_system/image_watermark/data_source.py new file mode 100644 index 0000000..845634f --- /dev/null +++ b/src/class_management_system/image_watermark/data_source.py @@ -0,0 +1,35 @@ +from nonebot import require +from io import BytesIO +from nonebot.adapters.cqhttp import MessageSegment +from PIL.PngImagePlugin import PngImageFile +from pandas import DataFrame + +Watermark = require("tools").Watermark + + +class SetWatermark: + def __init__(self, file, user_info: DataFrame): + self.file = file + self.user_info = user_info + + def image(self): + """发送图片""" + img_bytes = BytesIO() + self.img.save(img_bytes, format="JPEG") + return MessageSegment.image(img_bytes) + + async def draw(self) -> PngImageFile: + """绘制""" + async with Watermark( + url=self.file, + text=f'{self.user_info["姓名"]}\n{self.user_info["班级"]}', + size=100, + xy=(50, 50)) as res: + return res.img + + async def __aenter__(self): + self.img = await self.draw() + return self + + async def __aexit__(self, *args): + ... diff --git a/src/class_management_system/moral_education/README.md b/src/class_management_system/moral_education/README.md new file mode 100644 index 0000000..da7b0bc --- /dev/null +++ b/src/class_management_system/moral_education/README.md @@ -0,0 +1 @@ +# 德育分,表现分记录 \ No newline at end of file diff --git a/src/class_management_system/moral_education/__init__.py b/src/class_management_system/moral_education/__init__.py new file mode 100644 index 0000000..249d837 --- /dev/null +++ b/src/class_management_system/moral_education/__init__.py @@ -0,0 +1,85 @@ +from nonebot import on_command, require +from nonebot.rule import T_State +from nonebot.adapters.cqhttp import Bot, MessageEvent, Message, GroupMessageEvent +from .data_source import AddScoreLog, SaveDialog, GetMessage, ExportProve + +add_log = on_command("德育", aliases={"德育分"}) +export_log = on_command("导出德育") +export_prove = on_command("导出证明") + +rule = require("rule") +IS_USER = rule.IS_USER +CLASS_CADRE = rule.CLASS_CADRE +DORM_ADMIN = rule.DORM_ADMIN + + +def get_names(message: str): + names = message.split(" ") + new_names = [] + for name in names: + if name: + new_names.append(name) + return new_names + + +@add_log.handle() +async def add_log_handle(bot: Bot, event: MessageEvent, state: T_State): + """拆分出姓名""" + if await IS_USER(bot, event, state): + users = event.get_plaintext().split() + state["users"] = None + if users: + if await CLASS_CADRE(bot, event, state): + state["users"] = users + else: + await add_log.finish("歪,你不是班干部不能在后面写别人名字呢!") + else: + await add_log.finish() + + +@add_log.got("msg", prompt="说明一下这是什么事情吧。 ^_^") +async def add_log_got(bot: Bot, event: MessageEvent, state: T_State): + message = GetMessage(Message(state["msg"])) + if message.images: + state["images"] = message.images + if message.text: + state["msg"] = message.text + else: + await add_log.reject(Message("咦,怎么只有图片呀,你倒是说明一下呀,哼~")) + + +@add_log.got("images", prompt="图呢?") +async def add_log_got_images(bot: Bot, event: MessageEvent, state: T_State): + images = state["images"] + if isinstance(images, str): + images = GetMessage(Message(images)).images + if images: + names = state["users"] or state["user_info"]['姓名'] + async with AddScoreLog(state["user_info"]["班级"], names, event.user_id, + state["user_info"]["学号"], images, state["msg"]) as res: + await add_log.finish(res.text()) + else: + await add_log.finish("不给图就不给加!(☆-v-)") + + +@export_log.handle() +async def export_log_handle(bot: Bot, event: GroupMessageEvent, state: T_State): + if await CLASS_CADRE(bot, event, state): + async with SaveDialog(state["class_cadre"]["班级"], get_names(event.get_plaintext())) as res: + if res.not_value: + await export_log.finish(res.text()) + else: + await bot.call_api("upload_group_file", group_id=event.group_id, file=res.file_path, name=res.name) + + +@export_prove.handle() +async def export_prove_handle(bot: Bot, event: GroupMessageEvent, state: T_State): + if await CLASS_CADRE(bot, event, state): + async with ExportProve(state["class_cadre"]["班级"], get_names(event.get_plaintext())) as res: + if res.not_value: + await export_prove.finish(res.text()) + else: + for i in res.zip(): + await export_prove.send(i) + await bot.call_api("upload_group_file", group_id=event.group_id, file=res.file_path, name=res.name) + diff --git a/src/class_management_system/moral_education/config.py b/src/class_management_system/moral_education/config.py new file mode 100644 index 0000000..1b548c8 --- /dev/null +++ b/src/class_management_system/moral_education/config.py @@ -0,0 +1,6 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + score_log = "score_log" + user_table = "user_info" diff --git a/src/class_management_system/moral_education/data_source.py b/src/class_management_system/moral_education/data_source.py new file mode 100644 index 0000000..ca84b98 --- /dev/null +++ b/src/class_management_system/moral_education/data_source.py @@ -0,0 +1,203 @@ +from nonebot import require +from typing import List, Union +from .config import Config +from aiohttp import ClientSession +from zipfile import ZipFile, ZIP_DEFLATED +from pandas import DataFrame, Series +from json import loads, dumps +from os import path +from datetime import datetime +from nonebot.adapters.cqhttp import Message, MessageSegment + +config = Config() +mysql = require("botdb").MySQLdbMethods() +readfile = require("readfile").ReadFile("data", "class_system") + + +def select_names(names, typeof="name"): + """查看是否有需要查找的用户的名字""" + return f" and {typeof} in({','.join(['%s'] * len(names))})" if names else "" + + +class Image: + def __init__(self, data): + self.name = data.get("file").split(".")[0] + self.url = data.get("url") + + +class GetMessage: + def __init__(self, message: Message): + self.text = "" + self.images: List[Image] = [] + for msg in message: + if msg.type == "text": + txt = msg.data.get("text").strip() + if txt: + self.text = txt + elif msg.type == "image": + self.images.append(Image(msg.data)) + + +class AddScoreLog: + def __init__(self, class_name: str, + name: Union[str, list], + user_id: Union[int, list], + student_id: Union[str, list], + images: List[Image], + explain_reason: str): + self.class_name = class_name + self.name = name + self.image = images[0] + self.user_id = user_id + self.student_id = int(student_id) + self.explain_reason = explain_reason + self.reply = "" + self.image_path = path.join(self.class_name, "images") + self.prove_file = path.join(self.class_name, "prove.json") + + async def read_prove(self): + """读取证明""" + try: + return loads(await readfile.read(self.prove_file)) + except FileNotFoundError: + """读取失败时候创建新证明""" + prove = {"all_file": [], "user_file": {}} + readfile.mkdir(self.class_name) + await readfile.write(self.prove_file, dumps(prove)) + return prove + + async def save_prove(self): + """保存证明""" + await readfile.write(self.prove_file, dumps(self.prove)) + + async def query_user(self) -> DataFrame: + """按照班级查找用户""" + await mysql.execute(f"select 姓名, qq, 学号 from {config.user_table} where 班级=%s{select_names(self.name, '姓名')}", + [self.class_name, *self.name]) + return mysql.form() + + async def add_logger(self, name, user_id, student_id): + await mysql.execute_commit(f"insert into {config.score_log} " + "(class_name, name, qq, student_id, explain_reason, prove) value (%s,%s,%s,%s,%s,%s)", + [self.class_name, name, user_id, student_id, self.explain_reason, self.image.name]) + + async def save_image(self): + """将图片保存到本地""" + readfile.mkdir(self.image_path) + if self.image.name not in self.prove["all_file"]: + self.prove["all_file"].append(self.image.name) + async with ClientSession() as session: + async with session.get(self.image.url) as res: + data = await res.read() + await readfile.write(path.join(self.image_path, f"{self.image.name}.jpg"), data, mode="wb") + + async def __aenter__(self): + self.prove = await self.read_prove() + if isinstance(self.name, list): + self.user_info = await self.query_user() + for i, user in self.user_info.iterrows(): + self.reply += user["姓名"] + "," + await self.add_logger(user["姓名"], user["qq"], user["学号"]) + else: + await self.add_logger(self.name, self.user_id, self.student_id) + self.reply += "德育日志添加成功!!" + return self + + def text(self): + return Message(self.reply) + + async def __aexit__(self, *args): + await self.save_image() + await self.save_prove() + + +class SaveDialog: + def __init__(self, class_name: str, names: list): + self.date = datetime.now() + self.class_name = class_name + self.names = names + self.class_file = path.join(readfile.path, class_name) # 班级文件路径 + self.not_value = True # 是否为空数据 + self.file_path = "" + self.reply = "" + + def save(self): + """保存到本地文件""" + try: + self.data.to_excel(self.file_path, index=False) + except FileNotFoundError: + readfile.mkdir(self.class_name) + self.save() + + def file_name(self, suffix: str): + """根据班级名+时间保存为文件名""" + if self.names: + file_name = '_'.join(set(self.data["姓名"])) + else: + file_name = self.class_name + date = str(self.date).split(".")[0].replace(" ", "-").replace(":", "-") + return file_name + date + "." + suffix + + @staticmethod + def set_data(data: DataFrame) -> DataFrame: + """设置数据,将说明与时间相结合,并且添加附加分名称与分数""" + data["说明"] = [str(data.loc[i]["log_time"].date()) + data.loc[i]["说明"] for i in range(data.shape[0])] + data["附加分名称"], data["分数"] = "", "" + data.drop(columns="log_time", axis=0, inplace=True) + data.sort_values(by="姓名", ascending=False, inplace=True) # 按照姓名排序 + return data[["学号", "姓名", "附加分名称", "分数", "说明"]] + + async def get_data(self) -> DataFrame: + """查找用户,并且按照当月筛选出用户""" + await mysql.execute( + f"select student_id 学号, name 姓名, explain_reason 说明, log_time, prove from {config.score_log} " + f"where year(log_time)=%s and month(log_time)=%s and class_name=%s{select_names(self.names)}", + [self.date.year, self.date.month, self.class_name, *self.names]) + return mysql.form() + + async def __aenter__(self): + data: DataFrame = await self.get_data() + if data.shape[0]: + self.not_value = False + self.data: DataFrame = self.set_data(data) + self.name = self.file_name("xlsx") # 文件名 + self.file_path = path.join(self.class_file, self.name) + self.save() + else: + self.reply = "未发现日志!!!" + return self + + def text(self): + return Message(self.reply) + + async def __aexit__(self, *args): + ... + + +class ExportProve(SaveDialog): + def __init__(self, class_name: str, names: list): + super().__init__(class_name, names) + + async def __aenter__(self): + self.data: DataFrame = await self.get_data() + if self.data.shape[0]: + self.not_value = False + self.name = self.file_name("zip") # 文件名 + self.file_path = path.join(self.class_file, self.name) + self.zip() + else: + self.reply = "未发现日志!!!" + return self + + def zip(self): + prove = set(self.data["prove"]) + length = len(prove) + yield "开始进行打包..." + with ZipFile(self.file_path, "w", ZIP_DEFLATED) as zip_file: + for img in prove: + try: + zip_file.write(path.join(self.class_file, "images", f"{img}.jpg"), f"{img}.jpg") + except FileNotFoundError as err: + length -= 1 + print(err) + yield f"打包完成\n文件数量:{length}\n开始发送请耐心等待..." diff --git a/src/class_management_system/push_class_info/__init__.py b/src/class_management_system/push_class_info/__init__.py new file mode 100644 index 0000000..a5b8b28 --- /dev/null +++ b/src/class_management_system/push_class_info/__init__.py @@ -0,0 +1,58 @@ +from nonebot import on_command, require +from .data_source import InsertUser +from nonebot.adapters.cqhttp import Bot, GroupMessageEvent, Message +from nonebot.rule import T_State +from typing import Union +import re + +rule = require("rule") +permission = require("permission") +TEACHER = permission.TEACHER +MASTER = rule.MASTER +push_user = on_command("导入班级信息", aliases={"导入班级"}) + + +def re_docs(text: str) -> str: + return re.search(r"https://docs.qq.com/sheet/\S[^\"']+", str(text).replace("\\", ""), re.I).group() + + +def get_url(message: Union[Message, str]) -> str: + url = None + if isinstance(message, Message): + for msg in message: + if msg.type == "json" or msg.type == "xml": + url = re_docs(msg.data["data"]) + elif msg.type == "text": + url = re_docs(msg.data["text"]) + if url: + return url + else: + return re_docs(message) + + +def get_text(message: Message): + """获取文本""" + for msg in message: + if msg.type == "text": + return msg.data.get("text").strip() + + +@push_user.handle() +async def push_user_handler(bot: Bot, event: GroupMessageEvent, state: T_State): + if await MASTER(bot, event, state): + url = get_url(event.message) + if url: + state["param"] = url + else: + await push_user.finish() + + +@push_user.got("param", "请转发腾讯在线文档,或者在线文档的链接!!!") +async def push_user_got(bot: Bot, event: GroupMessageEvent, state: T_State): + url = get_url(state["param"]) + if url: + # 获取班级名称 + class_name = state["all_group"]["class_name"][state["all_group"]["class_group"].index(event.group_id)] + async with InsertUser(url, class_name) as res: + await push_user.finish(res.text()) + diff --git a/src/class_management_system/push_class_info/config.py b/src/class_management_system/push_class_info/config.py new file mode 100644 index 0000000..67cd2cb --- /dev/null +++ b/src/class_management_system/push_class_info/config.py @@ -0,0 +1,35 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + user_table = "user_info" + user_table_param_not_null = { + "姓名": str, + "序号": int, + "学号": int, + "性别": str, + "联系方式": int + } + user_table_param_type = { + "序号": int, + "姓名": str, + "班级": str, + "学号": int, + "性别": str, + "联系方式": int, + "身份证号": str, + "出生日期": str, + "寝室": str, + "寝室长": str, + "微信": str, + "QQ": int, + "邮箱": str, + "民族": str, + "籍贯": str, + "职位": str, + "团员": str, + "入党积极分子": str, + "团学": str, + "社团": str, + "分数": int + } diff --git a/src/class_management_system/push_class_info/data_source.py b/src/class_management_system/push_class_info/data_source.py new file mode 100644 index 0000000..be148c8 --- /dev/null +++ b/src/class_management_system/push_class_info/data_source.py @@ -0,0 +1,173 @@ +from aiohttp import ClientSession +from pandas import DataFrame, Series +from datetime import datetime, timedelta +from aiomysql import IntegrityError +from nonebot import require +from nonebot.adapters.cqhttp import Message +import re +from .config import Config + + +config = Config() +mysql = require("botdb").MySQLdbMethods() +main_url = "https://docs.qq.com/dop-api/opendoc" +headers = { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/94.0.4606.61 Safari/537.36 ", + "Upgrade-Insecure-Requests": "1", + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + "appName": "Opera" +} + + +class Frame: + def __init__(self, data: list): + data = self._raw_data(data) + self.columns = data[0][-1] + 1 + self.rows = data[0][-3] + 1 + self.raw_data = data[1] + self.data = self._data_frame() + + def _data_frame(self): + """提取数据""" + df = [] + index = 0 + # 将列表转为二维 + for i in self.raw_data: + if not int(i) % self.columns: + df.append([]) + index += 1 + value = self.raw_data[i].get("2") + df[index - 1].append(value[1] if value else value) + # 清除空行空列 + df = DataFrame(df, dtype="object").dropna(axis=0, how='all').dropna(axis=1, how='all') + index = 0 + columns = False + # 提取columns,标题 + for i in df.values: + index += 1 + if not all(i) and not columns: + continue + elif not columns: + return DataFrame(df.values[index:], columns=list(i), dtype="object") + + @staticmethod + def _raw_data(data): + """解析数据,并且可以得出行列与出未加工的数据""" + for values in data["clientVars"]["collab_client_vars"]["initialAttributedText"]["text"][0]: + for value in values: + if isinstance(value["c"][1], dict): + for i in value: + if isinstance(value[i], list): + return value[i] + + @staticmethod + def get_excel_date(date_int: int): + """对于excel日期进行转换,以便写入数据""" + if date_int: + return str((datetime(1899, 12, 30) + timedelta(int(date_int) + 1)).date()) + return None + + def reset_date(self, typeof: str = "出生日期"): + """重置表格日期表格""" + try: + self.data[typeof] = [self.get_excel_date(i) for i in self.data[typeof]] + except IndexError: + ... + finally: + return self + + +async def get(url: str, params: dict = None) -> Frame: + params = params or [] + async with ClientSession() as session: + async with session.get(url, headers=headers, params=params) as res: + return Frame(await res.json()) + + +class InsertUser: + """ + 写入班级信息 + """ + def __init__(self, url: str, class_name: str): + self.url = url + self.class_name = class_name + self.reply = Message() + + def get_params(self) -> dict: + """提取URL所需params""" + params = { + "outformat": 1, + "normal": 1 + } + url = self.url.split("/")[-1] + param = re.findall(r"^\w+|tab=\w+", url) + params["id"] = param[0] + if len(param) > 1: + params["tab"] = param[1].replace("tab=", "") + return params + + def set_sql(self, params: Series): + """ + 从DataFrame中提取出的每一个Series + 筛选出列名,然后筛选出必填选项,也就是不能为空的参数 + :return: sql语句, sql参数(数据) + """ + keys = set(params.keys()) & set(config.user_table_param_type.keys()) + null_param = set(config.user_table_param_not_null.keys()) - keys + for i in keys: + if params[i]: + params[i] = config.user_table_param_type[i](params[i]) + if not null_param: + sql = f"insert into {config.user_table} ({','.join([*keys, '班级'])}) value ({','.join(['%s'] * (len(keys) + 1))})" + param = list([*params.loc[keys].values, self.class_name]) + return sql, param + else: + return None, null_param + + @staticmethod + async def execute_commit(sql: str, params: list = None): + """向数据库写入数据""" + params = params or [] + try: + await mysql.execute_commit(sql, params) + except IntegrityError: + """数据已存在""" + + async def insert(self): + """写入数据库""" + try: + for i, v in enumerate(self.data.loc): # type: Series + sql, params = self.set_sql(v) + if sql and params: + await self.execute_commit(sql, params) + elif params: + self.reply.append(f"没缺少必要参数有”{','.join(params)}“并且该参数不能为空") + break + except KeyError as err: + """暂时未知的报错原因,大概原因是因为索引从0开始没有len所描述的最后一位所以报错""" + if isinstance(err.args[0], int): + self.reply.append("成功更新班级成员信息,可以使用查询来查看自己信息,如有错误需要修改可以使用修改命令修改本人信息!") + else: + self.reply.append("缺少必要参数%s,并且该参数不能为空!!!" % err) + + async def delete_user_info(self): + print(self.class_name) + # await mysql.execute_commit("delete from user_info where 班级=%s", [self.class_name]) + + async def __aenter__(self): + data = (await get(main_url, self.get_params())).reset_date() + # self.class_name = await self.get_class_name() + await self.delete_user_info() + self.data = data.data + await self.insert() + return self + + def text(self): + if isinstance(self.reply, Message): + return self.reply + return Message(self.reply) + + async def __aexit__(self, *args): + ... diff --git a/src/class_management_system/query_user/README.md b/src/class_management_system/query_user/README.md new file mode 100644 index 0000000..b19c1ee --- /dev/null +++ b/src/class_management_system/query_user/README.md @@ -0,0 +1,7 @@ +# 查询用户 + +编写功能 +- 查询 + - 查询 姓名 qq 序号 学号 +- at + - at 姓名 qq 序号 学号 \ No newline at end of file diff --git a/src/class_management_system/query_user/__init__.py b/src/class_management_system/query_user/__init__.py new file mode 100644 index 0000000..c697287 --- /dev/null +++ b/src/class_management_system/query_user/__init__.py @@ -0,0 +1,55 @@ +from nonebot import on_command, require +from nonebot.adapters.cqhttp import Bot, Message, GroupMessageEvent +from nonebot.rule import T_State +from .data_source import QueryUser, AtUser + + +CLASS_GROUP = require("permission").CLASS_GROUP +query_user = on_command("查询", aliases={"查找", "搜索"}) +at_user = on_command("at", aliases={"艾特", "@"}) + + +def get_text(message: Message) -> str: + """获取文本""" + for msg in message: + if msg.type == "text": + return msg.data.get("text").strip() + if msg.type == "at": + return " " + str(msg.data.get("qq")) + + +@query_user.handle() +async def query_user_handle(bot: Bot, event: GroupMessageEvent, state: T_State): + """获取查询的用户,如果没有填写查询对象则默认查询自己""" + if await CLASS_GROUP(bot, event): + features = get_text(event.message) + state["param"] = features.split() if features else [event.get_user_id()] + else: + await query_user.finish() + + +@query_user.got("param") +async def query_user_got(bot: Bot, event: GroupMessageEvent, state: T_State): + async with QueryUser(state["param"], event.group_id) as res: + await query_user.finish(res.text()) + + +@at_user.handle() +async def at_user_handle(bot: Bot, event: GroupMessageEvent, state: T_State): + if await CLASS_GROUP(bot, event): + features = get_text(event.message) + if features: + state["param"] = features.split() + else: + await query_user.finish() + + +@at_user.got("param", prompt="请输入您需要at的成员名称/序号/qq/学号。") +async def at_user_got(bot: Bot, event: GroupMessageEvent, state: T_State): + if isinstance(state["param"], str): + state["param"] = state["param"].split() + if state["param"]: + async with AtUser(state["param"], event.group_id) as res: + if not res.is_err: + await at_user.finish(res.text()) + diff --git a/src/class_management_system/query_user/config.py b/src/class_management_system/query_user/config.py new file mode 100644 index 0000000..0e8ec40 --- /dev/null +++ b/src/class_management_system/query_user/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + user_table = "user_info" + teacher_table = "teacher" + class_table = "class_table" diff --git a/src/class_management_system/query_user/data_source.py b/src/class_management_system/query_user/data_source.py new file mode 100644 index 0000000..3a574a3 --- /dev/null +++ b/src/class_management_system/query_user/data_source.py @@ -0,0 +1,205 @@ +from nonebot import require +from nonebot.adapters.cqhttp import Message, MessageSegment +from .config import Config +from pandas import Series, DataFrame + +config = Config() +mysql = require("botdb").MySQLdbMethods() + + +class Query: + def __init__(self, param: list, group_id: int): + self.group_id = group_id + self.param = param + self.index = [] + self.user_name = [] + self.uid = [] + + def _split(self): + for i in self.param: # type: str + if i.isdigit(): + if len(i) < 4: + self.index.append(int(i)) + else: + self.uid.append(int(i)) + else: + self.user_name.append(i) + + @staticmethod + def _fmt_in(tag: str, al: list): + if al: + return f"{tag} in({','.join(['%s'] * len(al))})" + + def _where(self, is_user: bool = True): + fmt = [] + if is_user: + for i in [self._fmt_in("qq", self.uid), + self._fmt_in("学号", self.uid), + self._fmt_in("序号", self.index), + self._fmt_in("姓名", self.user_name)]: + if i: + fmt.append(i) + else: + for i in [self._fmt_in("qq", self.uid), + self._fmt_in("telephone", self.uid), + self._fmt_in("name", self.user_name)]: + if i: + fmt.append(i) + return " or ".join(fmt) + + async def __aexit__(self, *args): + ... + + +class QueryUser(Query): + def __init__(self, param: list, group_id: int): + super().__init__(param, group_id) + self.not_user = True + self.is_err = False + self.reply = Message() + self._split() + + async def _get_class_name(self): + """获取班级名称,并且查询是否为班级群,不是班级群则报错KeyError""" + await mysql.execute(f"select class_group, class_name from {config.class_table}") + df: DataFrame = mysql.form() + class_group = list(df['class_group']) + if self.group_id in class_group: + return "and 班级='%s'" % df["class_name"][class_group.index(self.group_id)] + raise KeyError + + async def get_user(self): + """获取用户,是否有该用户,如果有则对信息进行提取并筛选出需要展示的信息""" + try: + await mysql.execute(f"select * from {config.user_table} where ({self._where()}) {await self._get_class_name()}", + [*(self.uid * 2), *self.index, *self.user_name]) + data: DataFrame = mysql.form() + length = data.shape[0] # 提取的用户数量 + if length < 1: + self.reply.append("抱歉,未查询到该用户!!!") + self.not_user = True + elif length == 1: + self.user(data.loc[0]) + self.not_user = False + else: + self.users(data) + self.not_user = False + except KeyError: + self.is_err = True + self.reply = "此群并非班级群,无法使用查询!!!" + + async def get_teacher(self): + """获取教师""" + await mysql.execute(f"select * from {config.teacher_table} where ({self._where(False)})", + [*(self.uid * 2), *self.user_name]) + data: DataFrame = mysql.form() + length = data.shape[0] + teachers = [] + if length: + teachers.append("查询到为教师:") + for i in range(length): + teacher = data.loc[i] + teachers.append(f"姓名:{teacher['name']}\n" + f"QQ:{teacher['qq']}\n" + f"联系方式:{teacher['telephone']}") + self.reply = Message('\n-----\n'.join(teachers)) + + @staticmethod + def user_key(user: Series, auto_end: str = "\n"): + """从用户key中查看是否存在值,如果存在则返回该数据加key""" + def _(key, end: str = auto_end): + """ + :param key: 数据的key + :param end: 在结尾追加字符 + """ + value = user.get(key) + if value != "None" and value != "": + return f"{key}:{value}{end}" + return "" + return _ + + def user(self, user: Series): + info = self.user_key(user) + birthday, dorm = [""] * 2 + if user.get("出生日期"): + birthday = user["出生日期"].date() + if user.get("寝室长"): + if user['寝室长'] == '是': + dorm = f"{user['寝室']}(寝室长)" + else: + dorm = user["寝室"] + self.reply.append(f"{info('序号')}" + f"{info('姓名')}" + f"{info('性别')}" + f"{info('班级')}" + f"{info('社团')}" + f"{info('团学')}" + f"{info('团员')}" + f"{info('入党积极份子')}" + f"{info('民族')}" + f"寝室:{dorm}\n" + f"出生日期:{birthday}\n" + f"{info('学号')}" + f"{info('微信')}" + f"{info('联系方式')}" + f"{info('邮箱')}" + f"{info('分数', '')}") + + def users(self, users: DataFrame): + for i in range(users.shape[0]): + user = users.loc[i] + self.user(user) + if i < users.shape[0] - 1: + self.reply.append("\n-----\n") + + def text(self): + return self.reply + + async def __aenter__(self): + """ + 查找用户,查看是否有用户,并且没有出现报错,如果没有报错并且没有用户则进行查找是否为教师 + """ + await self.get_user() + if not self.is_err and self.not_user: + await self.get_teacher() + return self + + +class AtUser(Query): + def __init__(self, param: list, group_id: int): + super().__init__(param, group_id) + self.not_user = True + self.is_err = False + self.users = [] + self.reply = Message() + self._split() + + async def _get_class_name(self): + """从数据库中检索出班级,提取班级名称,班级群号是唯一的""" + await mysql.execute(f"select class_name from {config.class_table} where class_group=%s", + [self.group_id]) + self.class_name = mysql.form()["class_name"][0] + + async def get_user(self): + await mysql.execute(f"select qq from {config.user_table} where ({self._where()}) and 班级=%s", + [*(self.uid * 2), *self.index, *self.user_name, self.class_name]) + self.users = mysql.form()["qq"] + + def text(self): + for i in self.users: + self.reply += MessageSegment.at(i) + return self.reply + + async def __aenter__(self): + try: + await self._get_class_name() + await self.get_user() + except KeyError: + self.is_err = True + return self + + + + + + diff --git a/src/class_management_system/reset_name/README.md b/src/class_management_system/reset_name/README.md new file mode 100644 index 0000000..e8cdc1e --- /dev/null +++ b/src/class_management_system/reset_name/README.md @@ -0,0 +1 @@ +# 群友重命名插件 \ No newline at end of file diff --git a/src/class_management_system/reset_name/__init__.py b/src/class_management_system/reset_name/__init__.py new file mode 100644 index 0000000..a13a848 --- /dev/null +++ b/src/class_management_system/reset_name/__init__.py @@ -0,0 +1,23 @@ +from pprint import pprint + +from nonebot import on_command, require +from nonebot.adapters.cqhttp import Bot, GroupMessageEvent +from .data_source import UserName + + +reset_name = on_command("重命名", aliases={"班级重命名", "学生重命名", "自动改备注"}) +IS_USER = require("rule").IS_USER + + +def get_group_user_id(users: list): + return [int(user["user_id"]) for user in users] + + +@reset_name.handle() +async def reset_name_handle(bot: Bot, event: GroupMessageEvent, state: dict): + if await IS_USER(bot, event, state): + group_user = get_group_user_id(await bot.get_group_member_list(group_id=event.group_id)) + async with UserName(group_user) as res: + await reset_name.send(res.load_text()) + await res.reset_name(event.group_id, bot.set_group_card) + await reset_name.finish(res.text()) diff --git a/src/class_management_system/reset_name/config.py b/src/class_management_system/reset_name/config.py new file mode 100644 index 0000000..4056e8e --- /dev/null +++ b/src/class_management_system/reset_name/config.py @@ -0,0 +1,6 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + user_table = "user_info" + diff --git a/src/class_management_system/reset_name/data_source.py b/src/class_management_system/reset_name/data_source.py new file mode 100644 index 0000000..f49910d --- /dev/null +++ b/src/class_management_system/reset_name/data_source.py @@ -0,0 +1,41 @@ +from nonebot import require +from nonebot.adapters.cqhttp import Message +from asyncio import sleep +from pandas import DataFrame + +from .config import Config + +mysql = require("botdb").MySQLdbMethods() +config = Config() + + +class UserName: + def __init__(self, users: list): + self.users = users + self.reply = "" + self.sleep = 5 + + async def select_user(self): + await mysql.execute(f"select * from {config.user_table} where qq in({','.join(['%s'] * len(self.users))})", + self.users) + return mysql.form() + + async def __aenter__(self): + self.all_user: DataFrame = await self.select_user() + return self + + def load_text(self): + return Message(f"请稍等,预计需要{self.all_user.shape[0] * self.sleep}秒完成!") + + async def reset_name(self, group_id: int, func): + for index, user in self.all_user.iterrows(): + await sleep(self.sleep) + # print(f"{user['姓名']} {user['寝室']} {user['联系方式']}") + await func(group_id=group_id, user_id=user["QQ"], card=f"{user['姓名']} {user['寝室']} {user['联系方式']}") + self.reply = "重命名成功" + + def text(self): + return Message(self.reply) + + async def __aexit__(self, *args): + ... diff --git a/src/class_management_system/set_data/README.md b/src/class_management_system/set_data/README.md new file mode 100644 index 0000000..6eccd2b --- /dev/null +++ b/src/class_management_system/set_data/README.md @@ -0,0 +1,11 @@ +# 增加或减少数据 + +- 增加 + - 院系 + - 教师 + - 专业 + - 班级 + +## 教师权限 +可以 + diff --git a/src/class_management_system/set_data/__init__.py b/src/class_management_system/set_data/__init__.py new file mode 100644 index 0000000..d1f69b0 --- /dev/null +++ b/src/class_management_system/set_data/__init__.py @@ -0,0 +1,208 @@ +import re +from nonebot import on_command, require +from nonebot.rule import T_State +from .data_source import ADD, DEL, Faculty, Expertise, Teacher, Class +from nonebot.adapters.cqhttp import Bot, MessageEvent, GroupMessageEvent, Message +permission = require("permission") +rule = require("rule") + +TEACHER = permission.TEACHER +MASTER = rule.MASTER + + +def teacher(matcher): + @matcher.handle() + async def _(bot: Bot, event: MessageEvent, state: T_State): + # 超级用户与教师才可执行 + if await TEACHER(Bot, event): + text = get_text(event.message) + if text: + state["param"] = text + else: + await matcher.finish() + + +def master(matcher): + @matcher.handle() + async def _(bot: Bot, event: GroupMessageEvent, state: T_State): + # 超级用户与教师才可执行 + if await MASTER(Bot, event, state): + state["param"] = get_text(event.message) or str(event.group_id) + else: + await matcher.finish() + + +def get_text(message: Message): + """获取文本""" + for msg in message: + if msg.type == "text": + return msg.data.get("text").strip() + + +# -------------------- 添加数据 -------------------- +add_faculty = on_command("添加院系", aliases={"新增院系", "添加学院", "新增学院"}) +add_expertise = on_command("添加专业", aliases={"新增专业"}) +add_teacher = on_command("添加教师", aliases={"新增教师"}) +add_class = on_command("添加班级", aliases={"新增班级"}) +teacher(add_faculty) +teacher(add_expertise) +teacher(add_teacher) +teacher(add_class) + + +# -------------------- 删除数据 -------------------- +del_faculty = on_command("删除院系", aliases={"移除院系", "剔除院系"}) +del_expertise = on_command("删除专业", aliases={"移除专业", "剔除专业"}) +del_teacher = on_command("删除教师", aliases={"移除教师", "剔除教师"}) +del_class = on_command("删除班级", aliases={"移除班级", "剔除班级"}) +teacher(del_faculty) +teacher(del_expertise) +teacher(del_teacher) +master(del_class) + + +# -------------------- 院系 -------------------- +@add_faculty.got("param", prompt="您需要”添加“的院系名称(例如:回复信息工程, 电气工程等等...)") +async def add_faculty_got(bot: Bot, event: MessageEvent): + text = get_text(event.message) + if text: + async with Faculty(ADD, text, event.user_id) as res: + await add_faculty.finish(res.text()) + + +# 删除 +@del_faculty.got("param", prompt="您需要”删除“的院系名称(例如:回复信息工程, 电气工程等等...)") +async def add_faculty_got(bot: Bot, event: MessageEvent): + text = get_text(event.message) + if text: + async with Faculty(DEL, text, event.user_id) as res: + await del_faculty.finish(res.text()) + + +# -------------------- 专业 -------------------- +@add_expertise.got("param", prompt="您需要”添加“的专业名称(例如:回复人工智能, 服装设计等等...)") +async def add_expertise_got(bot: Bot, event: MessageEvent, state: T_State): + state["param"] = get_text(event.message) + if state["param"]: + await add_expertise.send("请输入这个专业所在的院系。") + else: + await add_expertise.finish(Message("请认真填写内容!!!")) + + +@add_expertise.got("faculty") +async def expertise_in_faculty(bot: Bot, event: MessageEvent, state: T_State): + """ + param 为专业名称 + faculty 为该专业所在院系 + """ + state["faculty"] = get_text(event.message) + if state["param"] and state["faculty"]: + async with Expertise(ADD, state["param"], state["faculty"]) as res: + await add_expertise.finish(res.text()) + else: + await add_expertise.finish(Message("请认真填写内容!!!")) + + +# 删除 +@del_expertise.got("param", prompt="您需要”删除“的专业名称(例如:回复人工智能, 服装设计等等...)") +async def del_expertise_got(bot: Bot, event: MessageEvent, state: T_State): + if state["param"]: + async with Expertise(DEL, state["param"]) as res: + await del_expertise.finish(res.text()) + + +# -------------------- 教师 -------------------- +@add_teacher.got("param", prompt="请输入您需要添加的教师的姓名") +async def add_teacher_got(bot: Bot, event: MessageEvent, state: T_State): + state["param"] = get_text(event.message) + if state["param"]: + await add_teacher.send("请输入这位教师的QQ") + else: + await add_teacher.finish(Message("请认真填写内容!!!")) + + +@add_teacher.got("qq") +async def add_teacher_qq_got(bot: Bot, event: MessageEvent, state: T_State): + state["qq"] = get_text(event.message) + if state["qq"]: + await add_teacher.send("请输入这位教师的手机号") + else: + await add_teacher.finish(Message("请认真填写内容!!!")) + + +@add_teacher.got("tel") +async def add_teacher_tel_got(bot: Bot, event: MessageEvent, state: T_State): + state["tel"] = get_text(event.message) + if state["tel"]: + async with Teacher(ADD, state["param"], state["qq"], state["tel"], event.user_id) as res: + await add_teacher.finish(res.text()) + else: + await add_teacher.finish(Message("请认真填写内容!!!")) + + +# 删除 +@del_teacher.got("param", prompt="请输入这位教师的QQ号或姓名") +async def del_teacher_got(bot: Bot, event: MessageEvent, state: T_State): + if state["param"]: + is_name = "name" + if state["param"].isdigit(): + is_name = "qq" + async with Teacher(DEL, **{is_name: state["param"]}) as res: + await del_teacher.finish(res.text()) + else: + await add_teacher.finish(Message("请认真填写内容!!!")) + + +# -------------------- 班级 --------------------- +@add_class.got("param", prompt="请输入班级名称,(例如:人工智能2101等等...)如果您在此群使用该命令,此群将会变为班级群,并且您会被认定为此群班主任,请认真确定!!!") +async def add_class_got(bot: Bot, event: GroupMessageEvent, state: T_State): + class_name = state["param"] + if class_name: + split = re.findall(r"\S[^\d]+|\d+", class_name)[:2] + if len(class_name) > 1: + expertise, class_id = split + if expertise.isdigit(): + expertise, class_id = class_id, class_name + # 参数较多就这样写了 + async with Class(ADD, + class_name=class_name, + class_id=class_id, + class_group=event.group_id, + class_teacher=event.user_id, + expertise=expertise) as res: + await add_class.finish(res.text()) + else: + await add_class.finish("您缺少重要参数,您输入的班级必须包含班级名称+班级号,例如:人工智能2101等等...") + else: + await add_teacher.finish(Message("请认真填写内容!!!")) + + +# 删除 +@del_class.got("param", prompt="请输入班级名称或群号") +async def del_class_got(bot: Bot, event: GroupMessageEvent, state: T_State): + is_admin = True + state["del_type"] = "class_group" + if state["param"]: + if state["param"].isdigit(): + state["class_group"] = int(state["param"]) + is_admin = state["class_group"] in state["all_group"]["class_group"] + else: + state["del_type"] = "class_name" + state["class_group"] = state["param"] + is_admin = state["class_group"] in state["all_group"]["class_name"] + else: + state["class_group"] = event.group_id + if not is_admin: + await del_class.finish(Message("您并不是班主任,无权操作此群!!!")) + await del_class.send(Message(f"您确认要将“{state['class_group']}”从班级管理系统中永久删除吗?(是/否)")) + + +@del_class.got("is_del") +async def del_class_is_got(bot: Bot, event: GroupMessageEvent, state: T_State): + if "是" in state["is_del"]: + async with Class(DEL, **{state["del_type"]: state["class_group"]}) as res: + await del_class.finish(res.text()) + else: + await del_class.finish("已为您取消。") + + diff --git a/src/class_management_system/set_data/config.py b/src/class_management_system/set_data/config.py new file mode 100644 index 0000000..8b01b6c --- /dev/null +++ b/src/class_management_system/set_data/config.py @@ -0,0 +1,8 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + faculty = "faculty_table" + expertise = "expertise_table" + teacher = "teacher" + class_table = "class_table" diff --git a/src/class_management_system/set_data/data_source.py b/src/class_management_system/set_data/data_source.py new file mode 100644 index 0000000..fd006a8 --- /dev/null +++ b/src/class_management_system/set_data/data_source.py @@ -0,0 +1,209 @@ +from nonebot import require +from .config import Config +from nonebot.adapters.cqhttp import Message +from aiomysql import IntegrityError + +ADD = "add" +DEL = "del" +MySQLdbMethods = require("botdb").MySQLdbMethods +mysql = MySQLdbMethods() +config = Config() + + +# 学院院系 +class Faculty: + def __init__(self, + typeof: str, + faculty: str, + invitee: int + ): + """ + :param faculty: 需要添加的院系 + :param invitee: 添加人的QQ + """ + self.typeof = typeof + self.faculty = faculty # 院系 + self.invitee = invitee # 添加人 + self.is_err = False + self.reply_text = None + self.table_name = config.faculty + + async def __aenter__(self): + try: + if self.typeof == ADD: + # ----- 添加数据 ----- + await mysql.execute_commit( + f"insert into {self.table_name} value (%s,%s)", param=[ + self.faculty, self.invitee + ]) + # ----- 添加数据回复 ----- + self.reply_text = f"”{self.faculty}“添加成功。" + elif self.typeof == DEL: + # ----- 删除数据回复 ----- + await mysql.execute_commit(f"delete from {self.table_name} where faculty=%s", param=[ + self.faculty]) + # ----- 删除成功回复 ----- + self.reply_text = f"“{self.faculty}”已删除。" + except IntegrityError: + if self.typeof == DEL: + # ----- 删除失败回复 ----- + self.reply_text = f"“{self.faculty}”删除失败,可能”{self.faculty}“下还所在专业未删除!!!" + elif self.typeof == ADD: + # ----- 添加失败回复 ----- + self.reply_text = f"”{self.faculty}“已经添加,无需重复执行!!!" + self.is_err = True + return self + + def text(self) -> Message: + return Message(self.reply_text) + + async def __aexit__(self, *args): + ... + + +# 院系的专业 +class Expertise: + def __init__(self, + typeof, + expertise: str, + faculty: str = None + ): + self.typeof = typeof + self.faculty = faculty # 院系 + self.expertise = expertise # 专业 + self.is_err = False + self.reply_text = None + self.table_name = config.expertise + + async def __aenter__(self): + try: + if self.typeof == ADD: + # ----- 添加数据 ----- + await mysql.execute_commit( + f"insert into {self.table_name} value (%s,%s)", param=[ + self.faculty, self.expertise + ]) + # ----- 添加成功回复 ----- + self.reply_text = f"“{self.expertise}”添加成功,所在院系”{self.faculty}“。" + elif self.typeof == DEL: + # ----- 删除数据 ----- + await mysql.execute_commit(f"delete from {self.table_name} where expertise=%s", param=[ + self.expertise]) + # ----- 删除成功回复 ----- + self.reply_text = f"“{self.expertise}”已删除。" + except IntegrityError: + if self.typeof == ADD: + # ----- 添加失败回复 ----- + self.reply_text = f"”{self.expertise}“添加失败,可能是”{self.faculty}“不存在,或者是”{self.expertise}“已经添加过!!!" + self.is_err = True + return self + + def text(self) -> Message: + return Message(self.reply_text) + + async def __aexit__(self, *args): + ... + + +class Teacher: + def __init__(self, + typeof: str, + name: str = None, + qq: int = None, + tel: int = None, + invitee: int = None + ): + self.qq = qq # 教师QQ + self.tel = tel # 教师电话 + self.name = name # 教师姓名 + self.invitee = str(invitee) # 添加人 + self.typeof = typeof + self.is_err = False + self.reply_text = None + self.table_name = config.teacher + + async def __aenter__(self): + try: + if self.typeof == ADD: + # ----- 添加教师信息 ----- + await mysql.execute_commit(f"insert into {self.table_name} values (%s,%s,%s,%s)", param=[ + self.name, self.qq, self.invitee, self.tel + ]) + # ----- 添加成功回复 ----- + self.reply_text = f"”{self.name}“添加成功。" + elif self.typeof == DEL: + # # ----- 删除教师信息 ----- + if self.name: + sql = f"delete from {self.table_name} where name=%s" + else: + sql = f"delete from {self.table_name} where qq=%s" + await mysql.execute_commit(sql, param=[self.name or self.qq]) + # # ----- 删除成功回复 ----- + self.reply_text = f"”{self.name or self.qq}“已删除。" + except IntegrityError: + if self.typeof == ADD: + # ----- 添加失败回复 ----- + self.reply_text = f"“{self.name}”添加失败,可能重复添加某条数据,或者缺少某些数据!!!" + elif self.typeof == DEL: + # ----- 删除失败回复 ----- + self.reply_text = f"“{self.name}”删除失败,可能“{self.name}”下还有班级未删除!!!" + return self + + def text(self): + return Message(self.reply_text) + + async def __aexit__(self, *args): + ... + + +class Class: + def __init__(self, + typeof: str, + class_name: str = None, + class_id: int = None, + class_group: int = None, + class_teacher: int = None, + expertise: str = None + ): + self.typeof = typeof + self.class_name = class_name # 班级全称 + self.class_id = class_id # 班级id + self.class_group = class_group # 班级群 + self.class_teacher = class_teacher # 班主任 + self.expertise = expertise # 专业 + self.is_err = False + self.reply_text = None + self.table_name = config.class_table + + async def __aenter__(self): + try: + if self.typeof == ADD: + # ----- 添加班级 ----- + await mysql.execute_commit(f"insert into {self.table_name} values (%s,%s,%s,%s,%s)", param=[ + self.class_id, self.expertise, self.class_group, self.class_name, self.class_teacher + ]) + # ----- 添加成功回复 ----- + self.reply_text = f"”{self.class_name}“添加完成,您已成为”{self.class_name}“班主任,可以在此群导入班级表格。" + elif self.typeof == DEL: + # ----- 删除班级 ----- + if self.class_name: + sql = f"delete from {self.table_name} where class_name=%s" + else: + sql = f"delete from {self.table_name} where class_group=%s" + await mysql.execute_commit(sql, param=[self.class_name or self.class_group]) + # ----- 删除成功回复 ----- + self.reply_text = f"“{self.class_name or self.class_group}“已删除" + except IntegrityError: + if self.typeof == ADD: + # ----- 添加失败回复 ----- + self.reply_text = f"“{self.class_name}”添加失败,错误原因可能如下:\n1.教师不在数据库中。\n2.此专业存在。\n3.此班级已创建!!!" + elif self.typeof == DEL: + # ----- 删除失败回复 ----- + self.reply_text = f"“{self.class_name}”删除失败,请先清空该班级下的学生信息!!!" + return self + + def text(self): + return Message(self.reply_text) + + async def __aexit__(self, *args): + ... diff --git a/src/class_management_system/set_score/__init__.py b/src/class_management_system/set_score/__init__.py new file mode 100644 index 0000000..9243328 --- /dev/null +++ b/src/class_management_system/set_score/__init__.py @@ -0,0 +1,45 @@ +from nonebot import on_command, require +from nonebot.adapters.cqhttp import Bot, MessageEvent +from .data_source import SetScore, config, GiftScore + +rule = require("rule") +IS_USER = rule.IS_USER +CLASS_CADRE = rule.CLASS_CADRE + +add_score = on_command("加分", aliases={"添加分数", "减分", "扣分", "设置分数"}) +gift_score = on_command("转让", aliases={"赠与"}) + + +@add_score.handle() +async def add_score_handler(bot: Bot, event: MessageEvent, state: dict): + if (await CLASS_CADRE(bot, event, state)) and state["class_cadre"]["职位"] in config.score: + text = event.get_plaintext() + if text: + state["param"] = text.split() + else: + await add_score.finish() + + +@add_score.got("param", prompt="请输入成员名称与分数") +async def add_score_got(bot: Bot, event: MessageEvent, state: dict): + text = event.get_plaintext() + if text: + async with SetScore(state["class_cadre"]["班级"], text) as res: + await add_score.finish(res.text()) + + +@gift_score.handle() +async def gift_score_handle(bot: Bot, event: MessageEvent, state: dict): + if await IS_USER(bot, event, state): + text = event.get_plaintext() + if text: + state["param"] = text + + +@gift_score.got("param", prompt="请输入需要转让的那一位同学姓名与数量") +async def gift_score_got(bot: Bot, event: MessageEvent, state: dict): + text = event.get_plaintext() + if text: + async with GiftScore(state["user_info"]["班级"], event.user_id, state["user_info"]["分数"], text) as res: + await gift_score.finish(res.text()) + diff --git a/src/class_management_system/set_score/config.py b/src/class_management_system/set_score/config.py new file mode 100644 index 0000000..76ed20e --- /dev/null +++ b/src/class_management_system/set_score/config.py @@ -0,0 +1,6 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + user_table = "user_info" + score = ["学习委员"] diff --git a/src/class_management_system/set_score/data_source.py b/src/class_management_system/set_score/data_source.py new file mode 100644 index 0000000..2bc769c --- /dev/null +++ b/src/class_management_system/set_score/data_source.py @@ -0,0 +1,71 @@ +from nonebot import require +from .config import Config +from nonebot.adapters.cqhttp import Message +import re + +config = Config() +mysql = require("botdb").MySQLdbMethods() + + +class SetScore: + def __init__(self, class_name: str, text: str): + self.class_name = class_name + self.params = [i.split() for i in re.findall(r"\S.*?\d+", text)] + + async def set_score(self, score: int, names: list): + await mysql.execute_commit(f"update {config.user_table} set 分数=分数+%s " + f"where 班级=%s and 姓名 in({','.join(['%s'] * len(names))})", + [score, self.class_name, *names]) + + async def __aenter__(self): + if self.params: + for param in self.params: + await self.set_score(int(param[-1]), param[:-1]) + self.reply = "分数增改成功!" + else: + self.reply = "未写入加分名单!!!" + return self + + def text(self): + return Message(self.reply) + + async def __aexit__(self, *args): + ... + + +class GiftScore: + def __init__(self, class_name: str, user_id: int, my_score: int, to_name: str): + self.class_name = class_name + self.my_score = my_score + self.user_id = user_id + self.to_name = to_name + self.reply = "" + + async def gift_score(self): + await mysql.execute_commit(f"update {config.user_table} set 分数=分数+%s " + f"where 班级=%s and 姓名=%s", [self.score, self.class_name, self.to_name]) + await mysql.execute_commit(f"update {config.user_table} set 分数=分数-%s " + f"where 班级=%s and qq=%s", [self.score, self.class_name, self.user_id]) + + async def __aenter__(self): + self.to_name = self.to_name.split() + if len(self.to_name) > 1: + self.to_name, self.score = self.to_name[:2] + if self.score.isdigit(): + self.score = int(self.score) + if 0 < self.my_score >= self.score: + await self.gift_score() + self.reply = f"成功转让{self.score}给{self.to_name}" + else: + self.reply = f"抱歉,您的分数不足{self.score}!!!" + else: + self.reply = "输入有误,您的第二个参数应该是纯数字!!!" + else: + self.reply = "输入有误,确认输入格式:姓名 数量" + return self + + def text(self): + return Message(self.reply) + + async def __aexit__(self, *args): + ... diff --git a/src/class_management_system/shop/README.md b/src/class_management_system/shop/README.md new file mode 100644 index 0000000..626cc87 --- /dev/null +++ b/src/class_management_system/shop/README.md @@ -0,0 +1 @@ +# 商店插件 \ No newline at end of file diff --git a/src/class_management_system/shop/__init__.py b/src/class_management_system/shop/__init__.py new file mode 100644 index 0000000..d3770f2 --- /dev/null +++ b/src/class_management_system/shop/__init__.py @@ -0,0 +1,52 @@ +from nonebot import on_command, require +from nonebot.adapters.cqhttp import Bot, MessageEvent +from .data_source import get_url, AddGoods, SelectGoods + +add_goods = on_command("更新商品", aliases={"添加商品", "更新列表"}) +select_goods = on_command("查看商品", aliases={"商品列表", "兑换列表"}) +# purchase = on_command("购买", aliases={"兑换"}) +TEACHER = require("permission").TEACHER +IS_USER = require("rule").IS_USER + + +@add_goods.handle() +async def add_goods_handle(bot: Bot, event: MessageEvent, state: dict): + if await TEACHER(bot, event): + url = get_url(event.message) + if url: + state["url"] = url + else: + await add_goods.finish() + + +@add_goods.got("url", prompt="请发送在线文档或者链接") +async def add_goods_got(bot: Bot, event: MessageEvent, state: dict): + url = get_url(state["url"]) + if url: + async with AddGoods(event.user_id, url) as res: + await add_goods.finish(res.text()) + + +@select_goods.handle() +async def select_goods_handle(bot: Bot, event: MessageEvent, state: dict): + if await IS_USER(bot, event, state): + async with SelectGoods(event.user_id) as res: + await select_goods.finish(res.text()) + + +# @purchase.handle() +# async def purchase_handle(bot: Bot, event: MessageEvent, state: dict): +# if await IS_USER(bot, event, state): +# text = event.get_plaintext() +# if text: +# state["param"] = text + + +# @purchase.got("param", prompt="请输入序号或名称") +# async def purchase_got(bot: Bot, event: MessageEvent, state: dict): +# if not isinstance(state["param"], list): +# state["param"] = state["param"].split() +# if state["param"]: +# ... + + diff --git a/src/class_management_system/shop/config.py b/src/class_management_system/shop/config.py new file mode 100644 index 0000000..458f2d7 --- /dev/null +++ b/src/class_management_system/shop/config.py @@ -0,0 +1,5 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + shop_table = "shop" diff --git a/src/class_management_system/shop/data_source.py b/src/class_management_system/shop/data_source.py new file mode 100644 index 0000000..5dc01d4 --- /dev/null +++ b/src/class_management_system/shop/data_source.py @@ -0,0 +1,93 @@ +from nonebot import require +from nonebot.adapters.cqhttp import Message +from pandas import DataFrame +from .config import Config + +MySQLdbMethods = require("botdb").MySQLdbMethods +tools = require("tools") +get_url = tools.get_url +GetDocsSheet = tools.GetDocsSheet +mysql = MySQLdbMethods() +config = Config() + + +class AddGoods: + """添加商品""" + def __init__(self, user_id: int, url: str): + self.url = url + self.user_id = user_id + self.reply = "" + + async def get_sheet(self) -> DataFrame: + async with GetDocsSheet(self.url) as res: + return res.data + + def text(self): + return Message(self.reply) + + async def delete_goods(self): + """删除原本的所有商品""" + await mysql.execute_commit(f"delete from {config.shop_table} where teacher=%s", + [self.user_id]) + + async def __aenter__(self): + """ + 获取后删除商品然后写入新商品 + """ + self.data = await self.get_sheet() + await self.delete_goods() + if self.data.shape[0]: + for k, v in self.data.iterrows(): + await self.insert(v["奖励"], v["分数"]) + self.reply = "商品更新完成。" + else: + self.reply = "商品已全部清空!!!" + return self + + async def insert(self, name: str, price: int): + """写入商品""" + await mysql.execute_commit(f"insert into {config.shop_table} value (%s,%s,%s)", + [self.user_id, name, price]) + + async def __aexit__(self, *args): + ... + + +class SelectGoods: + """查询商品""" + def __init__(self, user_id: int): + self.user_id = user_id + self.reply = "" + + async def __aenter__(self): + self.data = await self.select_goods() + if self.data.shape[0]: + self.reply = "商品列表:" + for k, v in self.data.iterrows(): + self.reply += f"\n{v['shop_name']} | {v['shop_price']}分" + else: + self.reply = "抱歉,未发现上架可以兑换商品!!!" + return self + + async def select_goods(self) -> DataFrame: + """ + 依照用户的id与班级参数查询相关教师,在查找教师所上架的商品 + """ + await mysql.execute("SELECT shop_name,shop_price FROM class_table,user_info,shop where user_info.qq=%s and " + "user_info.班级=class_table.class_name and class_table.class_teacher=shop.teacher", + self.user_id) + return mysql.form() + + def text(self): + return Message(self.reply) + + async def __aexit__(self, *args): + ... + + +class Purchase: + def __init__(self, user_id: int, goods: list): + self.user_id = user_id + self.goods = goods + self.goods_index = [] + self.goods_name = [] diff --git a/src/class_management_system/table_processing/README.md b/src/class_management_system/table_processing/README.md new file mode 100644 index 0000000..87cc270 --- /dev/null +++ b/src/class_management_system/table_processing/README.md @@ -0,0 +1 @@ +# 表格处理插件 \ No newline at end of file diff --git a/src/class_management_system/table_processing/__init__.py b/src/class_management_system/table_processing/__init__.py new file mode 100644 index 0000000..67adb30 --- /dev/null +++ b/src/class_management_system/table_processing/__init__.py @@ -0,0 +1,77 @@ +from nonebot import on_command, require +from nonebot.rule import T_State +from .data_source import get_url, GetExcelNotUser, CheckUser +from nonebot.adapters.cqhttp import Bot, GroupMessageEvent + +push_user_info = on_command("未填写", aliases={"查表格", "查表", "为填表"}) +check_user = on_command("核对信息", aliases={"核验", "检查", "检查信息"}) +deep_check_user = on_command("深度检查", aliases={"严格检查", "深度核对"}) +super_check_user = on_command("超级核对", aliases={"超级检查"}) +IS_USER = require("rule").IS_USER + + +# 添加班级成员需要按群添加,因为群内唯一群号绑定一个班级名 +@push_user_info.handle() +async def _push_user_info_handle(bot: Bot, event: GroupMessageEvent, state: T_State): + if await IS_USER(bot, event, state): + url = get_url(event.message) + if url: + state["url"] = url + else: + await push_user_info.finish() + + +# 未接受到班级表格时 +@push_user_info.got("url", prompt="请发送班级表格") +async def _push_user_info_got(bot: Bot, event: GroupMessageEvent, state: T_State): + url = get_url(state["url"]) + if url: + async with GetExcelNotUser(state["user_info"]["班级"], url) as res: + await push_user_info.finish(res.text()) + + +def set_url(matcher): + @matcher.handle() + async def _(bot: Bot, event: GroupMessageEvent, state: T_State): + if await IS_USER(bot, event, state): + url = get_url(event.message) + if url: + state["url"] = url + else: + await matcher.finish() + + +# 检查班级信息 +set_url(check_user) +set_url(deep_check_user) +set_url(super_check_user) + + +# 未接受到班级表格时 +@check_user.got("url", prompt="请发送班级表格") +async def _push_user_info_got(bot: Bot, event: GroupMessageEvent, state: T_State): + url = get_url(state["url"]) + if url: + async with CheckUser(state["user_info"]["班级"], url) as res: + res.select_info() + await check_user.finish(res.text()) + + +# 未接受到班级表格时 +@deep_check_user.got("url", prompt="请发送班级表格") +async def _push_user_info_got(bot: Bot, event: GroupMessageEvent, state: T_State): + url = get_url(state["url"]) + if url: + async with CheckUser(state["user_info"]["班级"], url) as res: + res.deep_select_info() + await deep_check_user.finish(res.text()) + + +# 未接受到班级表格时 +@super_check_user.got("url", prompt="请发送班级表格") +async def _push_user_info_got(bot: Bot, event: GroupMessageEvent, state: T_State): + url = get_url(state["url"]) + if url: + async with CheckUser(state["user_info"]["班级"], url) as res: + res.super_select_info() + await super_check_user.finish(res.text()) diff --git a/src/class_management_system/table_processing/config.py b/src/class_management_system/table_processing/config.py new file mode 100644 index 0000000..68a5dce --- /dev/null +++ b/src/class_management_system/table_processing/config.py @@ -0,0 +1,5 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + user_table = "user_info" diff --git a/src/class_management_system/table_processing/data_source.py b/src/class_management_system/table_processing/data_source.py new file mode 100644 index 0000000..ccb43dc --- /dev/null +++ b/src/class_management_system/table_processing/data_source.py @@ -0,0 +1,123 @@ +from datetime import datetime, timedelta +from pprint import pprint +from typing import Union +from pandas import DataFrame, Series, merge, concat +from .config import Config +from nonebot import require +from nonebot.adapters.cqhttp import Message + +config = Config() +mysql = require("botdb").MySQLdbMethods() +tools = require("tools") +re_docs = tools.re_docs +get_url = tools.get_url +GetDocsSheet = tools.GetDocsSheet + + +class GetExcelNotUser: + def __init__(self, class_name: str, url: str): + self.url = url + self.class_name = class_name + self.reply = None + + async def select_user(self) -> DataFrame: + await mysql.execute(f"select 姓名, QQ from {config.user_table} where 班级=%s", + [self.class_name]) + return mysql.form() + + async def get_sheet(self) -> DataFrame: + try: + async with GetDocsSheet(self.url) as res: + return res.data + except IndexError: + self.reply = "查询失败,似乎表格没有设置成“所有人可编辑”请检查表格分享设置,或者重新分享一次!!" + + async def __aenter__(self): + self.sheet = await self.get_sheet() + self.users = await self.select_user() + return self + + async def get_names(self): + names = self.sheet.get("姓名") + if not names: + names = self.sheet.get("学生姓名") + return names + + def text(self): + if self.reply: + return Message(self.reply) + user = set(self.users.get("姓名")) - set(self.sheet.get("姓名")) + if user: + reply = ["未填写名单"] + for name in user: + reply.append(name) + reply = ' '.join(reply) + else: + reply = "未发现成员!!!" + return Message(reply) + + async def __aexit__(self, *args): + ... + + +class CheckUser(GetExcelNotUser): + async def __aenter__(self): + self.sheet = await self.get_sheet() + self.users = await self.select_user() + self.columns = self.get_column() + self.sheet = self.sheet[self.columns].astype("str") + self.users = self.users[self.columns].astype("str") + self.select_info() + self.reply: DataFrame + return self + + def super_select_info(self): + """ + 对班级进行超级校对,只要与班级信息相关的 + """ + mer = merge(self.users, self.sheet, how="inner") + err = concat([mer, self.users], join="inner", axis=0).drop_duplicates(keep=False) + self.reply = err + return err + + def deep_select_info(self): + """ + 校对班级用户 + 校对班级用户并且包括不在表格内的 + """ + mer = self.get_class() + err = merge(mer, self.users, how="inner") + err = concat([self.users, err], join="inner", axis=0).drop_duplicates(keep=False) + self.reply = err + return err + + def get_class(self): + c = self.sheet.get("班级") + if c is not None: + return self.sheet[c == self.class_name] + return self.sheet + + def select_info(self): + """ + 校对用户 + 查询检查班级用户,并且是表格中有的 + """ + mer = self.get_class() + err = merge(mer, self.users, how="inner") + err = concat([mer, err], join="inner", axis=0).drop_duplicates(keep=False) + self.reply = err + return err + + def get_column(self) -> list: + return list(set(self.sheet.columns) & set(self.users.columns)) + + async def select_user(self) -> DataFrame: + await mysql.execute(f"select 姓名,班级,联系方式,身份证号,qq,学号,民族,籍贯 from {config.user_table} where 班级=%s", + [self.class_name]) + return mysql.form() + + def text(self) -> Message: + text = self.reply + if text.shape[0]: + return Message(str(text)) + return Message("未发现错误!") diff --git a/src/plugins/animeres/README.md b/src/plugins/animeres/README.md new file mode 100644 index 0000000..f49b1c2 --- /dev/null +++ b/src/plugins/animeres/README.md @@ -0,0 +1,15 @@ +# 动漫资源获取 + +动漫资源获取是利用python爬取某网站的资源进行分类获取,根据网站给出的选项进行分类,选择选项后会去将选项的第一个选择出来对于网站资源的爬取只会去爬取第一页内容 + +### 命令 +```shell +资源 <参数> +``` + +![file](../../../tests/images/animeres1.png) + +FAQ: +资源类型可以用空格分割,以便于灵活的查找资源 + +![file](../../../tests/images/animeres2.png) \ No newline at end of file diff --git a/src/plugins/animeres/__init__.py b/src/plugins/animeres/__init__.py new file mode 100644 index 0000000..61085e7 --- /dev/null +++ b/src/plugins/animeres/__init__.py @@ -0,0 +1,42 @@ +from nonebot.rule import T_State +from nonebot import on_command +from nonebot.adapters.cqhttp import Bot, MessageEvent, Message +from .data_source import GetDMHY + + +anime_res = on_command("资源", aliases={"动漫资源"}) + + +@anime_res.handle() +async def anime_res_handle(bot: Bot, event: MessageEvent, state: T_State): + msg = event.get_plaintext() + if msg: + state["msg"] = msg + + +@anime_res.got("msg", prompt="动漫名字叫什么呀!") +async def anime_res_got(bot: Bot, event: MessageEvent, state: T_State): + msg = event.get_plaintext() + if not msg: + await anime_res.finish("...") + async with GetDMHY(msg) as res: + if not res: + await anime_res.finish("没有找到你想要的,看看是不是输错了吧!") + state["res"] = res + types = '\n'.join([f'{i+1}:{v}' for i, v in enumerate(res)]) + await anime_res.send(f"你需要什么类型的资源:{types}") + + +@anime_res.got("index") +async def anime_res_index(bot: Bot, event: MessageEvent, state: T_State): + index = state["index"] + if index.isdigit(): + res: GetDMHY = state["res"] + index = int(index) + if 0 < index <= res.length: + anime = res[res.tags[index-1]][0] + await anime_res.finish(f"资源名称:{anime.title}\n{anime.href}") + else: + await anime_res.finish("有这个序号吗?") + else: + await anime_res.finish("您输入的是什么呀,让你输入数字呢!") diff --git a/src/plugins/animeres/data_source.py b/src/plugins/animeres/data_source.py new file mode 100644 index 0000000..e01d8c0 --- /dev/null +++ b/src/plugins/animeres/data_source.py @@ -0,0 +1,60 @@ +from typing import Any, List, Tuple +from aiohttp import ClientSession, ClientTimeout +from feedparser import parse +from pydantic import BaseSettings + + +class AnimeRes(BaseSettings): + def __init__(self, value: dict, **values: Any): + super().__init__(**values) + self.title = value['title'] + try: + self.href = value["links"][1]['href'].split("&")[0] + except KeyError: + self.href = None + self.tags = [i["term"] for i in value["tags"]] + self.tag = value["tags"][0]['term'] + + class Config: + extra = "allow" + + +class GetDMHY: + main_url = "https://dmhy.anoneko.com/topics/rss/rss.xml" + + def __init__(self, keyword: str): + self.params = {"keyword": keyword} + + def classification(self) -> Tuple[dict, list]: + values, tags = {}, [] + for i in self.resources: + if i.href: + if values.get(i.tag): + values[i.tag].append(i) + else: + values[i.tag] = [i] + tags.append(i.tag) + return values, tags + + def __getitem__(self, item: str) -> List[AnimeRes]: + return self.values.get(item) + + def __iter__(self): + return iter(self.tags) + + async def get(self) -> List[AnimeRes]: + async with self.session.get(self.main_url, params=self.params) as res: + return [AnimeRes(i) for i in parse(await res.text()).get("entries")] + + def __bool__(self): + return bool(self.tags) + + async def __aenter__(self): + self.session = ClientSession(timeout=ClientTimeout(600)) + self.resources = await self.get() + self.values, self.tags = self.classification() + self.length = len(self.tags) + return self + + async def __aexit__(self, *args): + await self.session.close() diff --git a/src/plugins/chatbot/__init__.py b/src/plugins/chatbot/__init__.py new file mode 100644 index 0000000..671ad01 --- /dev/null +++ b/src/plugins/chatbot/__init__.py @@ -0,0 +1,52 @@ +from nonebot import on_command, on_message +from nonebot.permission import SUPERUSER +from nonebot.rule import T_State +from nonebot.adapters.cqhttp import Bot, MessageEvent, GroupMessageEvent +from .config import Config +from .data_source import AddChatWord, get_url, BotChat + + +# export_chat = on_command("导入词库", aliases={"添加词库", "更新词库"}) +on_chat = on_message(priority=100) +chat_ends = ["再见", "不聊了", "结束对话", "拜拜", "bye"] + + +# @export_chat.handle() +# async def export_chat_handle(bot: Bot, event: MessageEvent, state: T_State): +# if await SUPERUSER(bot, event): +# url = get_url(event.message) +# if url: +# state["url"] = url +# else: +# await export_chat.finish() +# +# +# @export_chat.got("url", prompt="请发送在线文档链接或卡片") +# async def export_chat_got(bot: Bot, event: MessageEvent, state: T_State): +# url = get_url(state["url"]) +# if url: +# async with AddChatWord(url) as res: +# await export_chat.finish(res.text()) +# else: +# await export_chat.finish("添加失败!!!") + + +@on_chat.handle() +async def on_chat_handle(bot: Bot, event: MessageEvent, state: T_State): + if event.to_me: + state["start"] = "开始聊天" + else: + await on_chat.finish() + + +@on_chat.got("start") +async def on_chat_got(bot: Bot, event: MessageEvent, state: T_State): + text = event.get_plaintext() + for txt in chat_ends: + if txt in text: + await on_chat.finish("拜拜!") + async with BotChat(text) as res: + if isinstance(event, GroupMessageEvent): + await on_chat.reject(await res.text()) + await on_chat.finish(await res.text()) + diff --git a/src/plugins/chatbot/config.py b/src/plugins/chatbot/config.py new file mode 100644 index 0000000..0ef390f --- /dev/null +++ b/src/plugins/chatbot/config.py @@ -0,0 +1,8 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + # Your Config Here + + class Config: + extra = "ignore" \ No newline at end of file diff --git a/src/plugins/chatbot/data_source.py b/src/plugins/chatbot/data_source.py new file mode 100644 index 0000000..a6df7e7 --- /dev/null +++ b/src/plugins/chatbot/data_source.py @@ -0,0 +1,132 @@ +from aiohttp import ClientSession +from nonebot import require +from pandas import DataFrame +from nonebot.adapters.cqhttp import Message +from json import dumps, loads +from random import choice +from typing import List + +tools = require("tools") +readfile = require("readfile").ReadFile("data", "chatword") +base_url = "http://i.itpk.cn/api.php" + + +def get_params(msg: str): + return { + "question": msg, + "limit": 8, + "api_key": "1447d08d8e018247e2ce829e9d0380e8", + "api_secret": "p782g4wij4b0" + } + + +GetDocsSheet = tools.GetDocsSheet +get_url = tools.get_url +bot_chat: dict = { + "words": [] +} + + +def chat_sorted(words: dict) -> List: + return [list(i) for i in sorted(words.items(), key=lambda x: -len(x[0]))] + + +class AddChatWord: + def __init__(self, url: str): + self.url = url + self.reply = "" + + async def get_sheet(self): + """获取腾讯文档""" + async with GetDocsSheet(self.url) as res: + return res + + async def __aenter__(self): + """ + 从腾讯文档中获取数据 + 取出转为dafaframe的数据 + 提取词库 + 保存到本地 + """ + self.sheet = await self.get_sheet() + self.data: dict = self.sheet.data + self.df: DataFrame = self.sheet.data + self.words = self.chat_word() + await self.save() + self.reply = "词库更新成功。" + return self + + def chat_word(self): + """ + 提取词库整理成字典 + 再将字典排序转为列表 + """ + words = {word["关键字"]: [txt for txt in word['随机回复词1': "随机回复词20"] if txt] for i, word in self.df.iterrows()} + return [list(i) for i in sorted(words.items(), key=lambda x: -len(x[0]))] + + def text(self): + return Message(self.reply) + + async def save(self): + """ + 将数据保存到本地 + """ + bot_chat["words"] = self.words + await readfile.write("chat_word.json", dumps(self.words)) + + async def __aexit__(self, *args): + ... + + +class BotChat: + def __init__(self, word: str): + self.word = word + + @staticmethod + async def data_in_msg(msg: str): + data = await readfile.read("data.json") + for i in data: + if i in msg: + return choice(data[i]) + + @staticmethod + async def get_message_reply(msg: str): + async with ClientSession() as session: + async with session.get(url=base_url, params=get_params(msg)) as resp: + try: + text = loads((await resp.text()).encode("utf-8")) + try: + return text["content"] + except KeyError: + return "抱歉,为对数据进行整理目前无法使用" + except ValueError: + return await resp.text() + + @staticmethod + async def get_words(): + """ + 从全局变量中查看是否存在词库 + 如果不存在则从本地取出 + :return: + """ + words = bot_chat["words"] + if not words: + words = loads(await readfile.read("data.json")) + bot_chat["words"] = words + return words + + async def text(self): + for i in [ + self.data_in_msg, + self.get_message_reply + ]: + msg = await i(self.word) + if msg: + return msg + + async def __aenter__(self): + # self.words = await self.get_words() + return self + + async def __aexit__(self, *args): + ... diff --git a/src/plugins/covid_19/__init__.py b/src/plugins/covid_19/__init__.py new file mode 100644 index 0000000..fe3f900 --- /dev/null +++ b/src/plugins/covid_19/__init__.py @@ -0,0 +1,41 @@ +from nonebot import get_driver, on_command +from aiohttp import ClientSession +from nonebot.adapters.cqhttp import Bot, MessageEvent, Message +from nonebot.rule import T_State +from .config import Config + +covid = on_command("疫情", aliases={"疫情查询"}) +get_covid = [] + + +@covid.handle() +async def covid_handler(bot: Bot, event: MessageEvent, state: T_State): + text = event.get_plaintext() + if text and event.user_id not in get_covid: + state["region"] = text + get_covid.append(event.user_id) + else: + await covid.finish("请勿频繁获取!!!") + + +@covid.got("region", prompt="请发送地区") +async def covid_got(bot: Bot, event: MessageEvent, state: T_State): + text = state["region"] + await covid.send(Message("正在获取中...")) + try: + async with ClientSession() as session: + async with session.get("https://api.iyk0.com/yq/", params={"msg": text}) as res: + data = await res.json() + if data["code"] == 200: + await covid.send(Message(f'{data["msg"]}\n' + f'查询地区:{data["查询地区"]}\n' + f'目前确诊:{data["目前确诊"]}\n' + f'死亡人数:{data["死亡人数"]}\n' + f'治愈人数:{data["治愈人数"]}\n' + f'新增确诊:{data["新增确诊"]}\n' + f'现存无症状:{data["现存无症状"]}\n' + f'更新时间:{data["time"]}')) + elif data["code"] == 202: + await covid.send(Message(data["msg"])) + finally: + get_covid.remove(event.user_id) diff --git a/src/plugins/covid_19/config.py b/src/plugins/covid_19/config.py new file mode 100644 index 0000000..0ef390f --- /dev/null +++ b/src/plugins/covid_19/config.py @@ -0,0 +1,8 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + # Your Config Here + + class Config: + extra = "ignore" \ No newline at end of file diff --git a/src/plugins/covid_19/data_source.py b/src/plugins/covid_19/data_source.py new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/encyclopedia/__init__.py b/src/plugins/encyclopedia/__init__.py new file mode 100644 index 0000000..92ffce3 --- /dev/null +++ b/src/plugins/encyclopedia/__init__.py @@ -0,0 +1,51 @@ +from nonebot import get_driver, on_command, on_notice +from aiohttp import ClientSession +from nonebot.adapters.cqhttp import Bot, MessageEvent, Message, PokeNotifyEvent, MessageSegment +from nonebot.rule import T_State +from .config import Config + +encyclopedia = on_command("百科", aliases={"百科搜索"}) +echo = on_command("echo") +get_covid = [] +poke = on_notice() + + +@echo.handle() +async def echo_handler(bot: Bot, event: MessageEvent): + await echo.finish(event.message) + + +@poke.handle() +async def poke_handle(bot: Bot, event: PokeNotifyEvent, state: T_State): + if event.is_tome(): + await poke.finish("不,不要戳我>_ Rule: + """ + 读取消息是否包含有av或bv号,或者存在bili的xml卡片 + 判断是否是消息事件,循环判断消息内是否存在xml如果不是xml将作为纯文本读取 + :return: Rule + """ + async def _bili_xml(bot: Bot, event: Event, state: T_State) -> bool: + if event.get_type() == "message": + event: MessageEvent + for msg in event.message: + if msg.type == "xml" or msg.type == "json": + pprint(msg.data["data"][0]) + text = re.search(r"https://b23.tv/\w+", str(msg.data["data"]).replace("\\", ""), re.I) + if text: + state["xml_url"] = await get_link_redirection(text.group()) + return True + else: + text = re.search(bv_av, event.get_plaintext(), flags=re.I) + if text: + state["xml_url"] = text.group() + return True + return False + return Rule(_bili_xml) + + +""" ====== matcher ====== """ + + +user_info = on_command("mid", aliases={"UP", "up", "https://space.bilibili.com/"}) +video_msg = on_message(rule=is_bili_id()) + + +@video_msg.handle() +async def _(bot: Bot, event: MessageEvent, state: T_State): + xml_url = state.get("xml_url") + text = re.search(bv_av, xml_url, flags=re.I) + if text: + async with GetVideoInfo(text.group()) as res: + await video_msg.finish(res.text()) + elif re.search(r"space.bilibili.com/\d+", xml_url): + # 处理用户的xml卡片 + async with GetUserInfo(re.search(r"\d+", xml_url).group()) as res: + await user_info.finish(res.text()) + + +@user_info.handle() +async def _(bot: Bot, event: MessageEvent): + text = event.get_plaintext() + if text: + async with GetUserInfo(text) as res: + await user_info.finish(res.text()) + + + + + + + diff --git a/src/plugins/get_bilibili_info/config.py b/src/plugins/get_bilibili_info/config.py new file mode 100644 index 0000000..6ffee40 --- /dev/null +++ b/src/plugins/get_bilibili_info/config.py @@ -0,0 +1,16 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + # 视频信息地址 + video_info_url = "https://api.bilibili.com/x/web-interface/view" + # 用户信息地址 + user_info_url = "http://api.bilibili.com/x/space/acc/info" + # 视频链接 + video_url = "https://www.bilibili.com/video/" + # 用户链接 + user_url = "https://space.bilibili.com/" + # 用户名搜索 + user_name_search_url = "https://api.bilibili.com/x/web-interface/search/type" + # bili直播地址 + bili_live_url = "https://live.bilibili.com/" diff --git a/src/plugins/get_bilibili_info/data_source.py b/src/plugins/get_bilibili_info/data_source.py new file mode 100644 index 0000000..5da92a8 --- /dev/null +++ b/src/plugins/get_bilibili_info/data_source.py @@ -0,0 +1,162 @@ +from aiohttp import ClientSession +from nonebot.adapters.cqhttp import MessageSegment, Message +from .config import Config +from typing import Optional, Union +from asyncio import get_event_loop +from pprint import pprint +import re + +BV = "bvid" +AV = "aid" +urls = Config() + + +async def get_link_redirection(url): + async with ClientSession() as session: + async with session.get(url) as reply: + return str(reply.url) + + +class MessageBar(Message): + def __getattr__(self, item): + return lambda *args, **kwargs: self.append(getattr(MessageSegment, item)(*args, **kwargs)) + + +""" ====== 获取视频信息 ====== """ + + +class GetVideoInfo: + def __init__(self, vid): + self.vid: str = vid + self.raw_data: Optional[dict] = None + self.data: Optional[dict] = None + self.code: Optional[int] = None + self.url = urls.video_url + + def params(self) -> dict: + """ + 将id号进行分析是bv还是av号,然后将号转为各自的参数字典 + :return: dict + """ + if re.match(r"av", self.vid, re.I): + return {AV: re.sub("av", "", self.vid, flags=re.I)} + elif re.match(r"bv", self.vid, re.I): + return {BV: self.vid} + return {} + + def text(self) -> Union[Message, None]: + """将json数据转为可读性高的Message实列""" + if self.code == 0 and self.data: + stat = self.data['stat'] + reply = (MessageBar() + .text(f"{self.data['title']}\n" + f"{'-' * 16}\n" + f"作者(up): {self.data['owner']['name']}\n" + f"作者id: {self.data['owner']['mid']}\n" + f"{'-' * 16}\n" + f"aid: av{self.data['aid']}\n" + f"bvid: {self.data['bvid']}\n" + f"cid: {self.data['cid']}\n") + .image(self.data["pic"]) + .text(f"\n播放量: {stat['view']} | 投币数: {stat['coin']} | 点赞数: {stat['like']}" + f"\n弹幕数: {stat['danmaku']} | 收藏数: {stat['favorite']} | 评论数: {stat['reply']}" + f"\n{'-' * 16}" + f"\n{self.data['desc']}\n" + f"链接: {self.url}")) + return reply + return None + + async def get(self): + """获取数据""" + async with self.session.get(urls.video_info_url, params=self.params()) as res: + data = await res.json() + self.raw_data = data + self.url += self.vid + self.code = data["code"] + self.data = data["data"] if self.code == 0 else self.data + + async def __aenter__(self): + self.session = ClientSession() + await self.get() + return self + + async def close(self): + await self.session.close() + + async def __aexit__(self, *args): + await self.close() + + +""" ====== 获取用户信息 ====== """ + + +class GetUserInfo: + def __init__(self, username: str): + self.username: str = username + self.url: Optional[str] = None + self.code: Optional[int] = None + self.data: Optional[dict] = None + self.params: Optional[dict] = None + + def user_silence(self): + """查看用户是否被封""" + return "\n账号状态: 被封" if self.data["silence"] else "" + + def live_status(self): + """查看用户是否在直播""" + live_room = self.data["live_room"] + if live_room["liveStatus"]: + return f"\n这位UP正在直播中-直通车: {urls.bili_live_url}{live_room['roomid']}" + return "" + + def text(self) -> Message: + return (MessageBar() + .text(self.data["name"]) + .text(f"\n性别: {self.data['sex']}" + f"\n生日: {self.data['birthday'] or '不知道'}" + f"\n等级: level{self.data['level']}" + f"\n用户id: {self.username}" + f"{self.user_silence()}") + .image(self.data["face"]) + .text(f"\n简介: {self.data['sign']}" + f"\n友情链接: {urls.user_url + self.username}" + f"{self.live_status()}")) + + async def get_user_info(self): + """ + 获取用户信息 + 判断数据是用户id还是用户名称,如果不是用户id则搜索用户并获取用户列表中的第一位作 + 获取到第一位用户的id后利用id再次发送请求获取改用户的详细信息 + """ + if self.username.isdigit(): + self.url = urls.user_info_url + self.params = {"mid": self.username} + await self.get() + else: + self.url = urls.user_name_search_url + self.params = { + "search_type": "bili_user", + "keyword": self.username, + "changing": "mid", + "page": 1, + } + await self.get() + self.username = str(self.data["result"][0]["mid"]) + await self.get_user_info() + + async def get(self): + async with self.session.get(self.url, params=self.params) as reply: + info = await reply.json() + self.code = info["code"] + self.data = info["data"] if self.code == 0 else self.data + + async def __aenter__(self): + self.session = ClientSession() + await self.get_user_info() + return self + + async def close(self): + await self.session.close() + + async def __aexit__(self, *args): + await self.close() diff --git a/src/plugins/peeping/__init__.py b/src/plugins/peeping/__init__.py new file mode 100644 index 0000000..4140de0 --- /dev/null +++ b/src/plugins/peeping/__init__.py @@ -0,0 +1,14 @@ +from nonebot import on_command +from nonebot.adapters.cqhttp import Bot, GroupMessageEvent, MessageSegment +from nonebot.permission import SUPERUSER +from .data_source import Peeping +from json import dumps + +peeping = on_command("窥屏", permission=SUPERUSER, block=False, priority=1) + + +@peeping.handle() +async def peeping_handle(bot: Bot, event: GroupMessageEvent): + async with Peeping() as res: + await peeping.send(MessageSegment.xml(res.get_xml())) + await peeping.finish(await res.get_send_msg()) diff --git a/src/plugins/peeping/card.json b/src/plugins/peeping/card.json new file mode 100644 index 0000000..c99539f --- /dev/null +++ b/src/plugins/peeping/card.json @@ -0,0 +1,35 @@ +{ + "app": "com.tencent.structmsg", + "config": { + "autosize": true, + "ctime": 1636171890, + "forward": true, + "token": "7264596b2c6ae2063f045293b78db1dc", + "type": "normal" + }, + "desc": "新闻", + "extra": { + "app_type": 1, + "appid": 100951776, + "msg_seq": 7027304746066715522, + "uin": 2711402357 + }, + "meta": { + "news": { + "action": "", + "android_pkg_name": "", + "app_type": 1, + "appid": 100951776, + "desc": "你感兴趣的视频都在B站", + "jumpUrl": "https://b23.tv/mGMAnD?share_medium=android&share_source=qq&bbid=XX5AE4A57C36D652CA32EB08BA6E8735D8FDF&ts=1636171884331", + "preview":"https://open.gtimg.cn/open/app_icon/00/95/17/76/100951776_100_m.png?t=1635933215?date=20211106", + "source_icon":"", + "source_url":"", + "tag":"哔哩哔哩", + "title":"MelodyKnit的个人空间" + } + }, + "prompt":"[分享]MelodyKnit的个人空间", + "ver":"0.0.0.1", + "view":"news" +} \ No newline at end of file diff --git a/src/plugins/peeping/data_source.py b/src/plugins/peeping/data_source.py new file mode 100644 index 0000000..98887c0 --- /dev/null +++ b/src/plugins/peeping/data_source.py @@ -0,0 +1,93 @@ +from pprint import pprint + +from aiohttp import ClientSession +from asyncio import sleep +from json import loads +from typing import Union, List +from json import dumps + +main_url = "http://melodyknit.club:8000/peeping" + +xml = """我爱死这个视频了这简直是视觉盛宴!!!""" + + +class Peeping: + card = { + "app": "com.tencent.structmsg", + "config": { + "autosize": True, + "ctime": 1636171890, + "forward": True, + "token": "7264596b2c6ae2063f045293b78db1dc", + "type": "normal" + }, + "desc": "新闻", + "extra": { + "app_type": 1, + "appid": 100951776, + "msg_seq": 7027304746066715522, + "uin": 2711402357 + }, + "meta": { + "news": { + "action": "", + "android_pkg_name": "", + "app_type": 1, + "appid": 100951776, + "desc": "你感兴趣的视频都在B站", + "jumpUrl": "https://b23.tv/mGMAnD?share_medium=android&share_source=qq&bbid=XX5AE4A57C36D652CA32EB08BA6E8735D8FDF&ts=1636171884331", + "preview": "https://open.gtimg.cn/open/app_icon/00/95/17/76/100951776_100_m.png?t=1635933215?date=20211106", + "source_icon": "", + "source_url": "", + "tag": "哔哩哔哩", + "title": "MelodyKnit的个人空间" + } + }, + "prompt": "[分享]MelodyKnit的个人空间", + "ver": "0.0.0.1", + "view": "news" + } + + def __init__(self, interval: int = 5): + self._xml = xml + self._session = ClientSession() + self.uid = None + self.interval = interval + self.img_url = f"{main_url}/image?uid=" + + async def _get(self, url, params=None) -> Union[dict, list]: + async with self._session.get(url, params=params) as res: + return loads(await res.read()) + + async def __aenter__(self): + self.uid = (await self._get(main_url))["uid"] + self.img_url += str(self.uid) + return self + + def get_xml(self) -> str: + return self._xml.format(url=self.img_url) + + def get_json(self): + # self.card["meta"]["news"]["source_icon"] = self.img_url + return dumps(self.card) + + @staticmethod + def _msg(info) -> str: + return (f"时间:{info['time']}\n" + f"IP:{info['host']}\n" + f"所在地:{info['referer']}\n" + f"设备:{info['ua']}") + + async def get_data(self) -> List[dict]: + await sleep(self.interval - 1) + return await self._get(main_url, params={"uid": self.uid}) + + async def get_send_msg(self) -> str: + data = await self.get_data() + if data: + return "\n---\n".join([self._msg(info) for info in data]) + else: + return "未发现" + + async def __aexit__(self, *ags): + await self._session.close() diff --git a/src/plugins/random_content/__init__.py b/src/plugins/random_content/__init__.py new file mode 100644 index 0000000..92780c5 --- /dev/null +++ b/src/plugins/random_content/__init__.py @@ -0,0 +1,13 @@ +from nonebot import on_command +from random import choice +from nonebot.adapters.cqhttp import Bot, MessageEvent, Message + +on_random = on_command("随机") + + +@on_random.handle() +async def random_handle(bot: Bot, event: MessageEvent, state: dict): + params = event.get_plaintext().split() + if len(params) > 1: + await on_random.finish(Message(choice(params))) + diff --git a/src/plugins/set_jetbot/__init__.py b/src/plugins/set_jetbot/__init__.py new file mode 100644 index 0000000..801f3e1 --- /dev/null +++ b/src/plugins/set_jetbot/__init__.py @@ -0,0 +1,62 @@ +from nonebot import on_command +from nonebot.adapters.cqhttp import Bot, MessageEvent +from aiohttp import ClientSession, ClientConnectionError + +cmd = on_command("bot") +host = None +port = 78 +is_run = False + + +class JetBot: + def __init__(self, command: str = None, typeof="move"): + self.cmd = command + self.typeof = typeof + self.reply = "" + + async def __aenter__(self): + self.session = ClientSession() + return self + + async def get(self): + global is_run + try: + async with self.session.get(f"http://{host}:{port}/login"): + is_run = True + return f"{host}连接成功!" + except ClientConnectionError: + is_run = False + return f"{host}未发现jetbot,连接失败!!!" + + async def post(self): + global is_run + try: + async with self.session.post(f"http://{host}:{port}/move", params={ + "cmd": self.cmd + }) as res: + if (await res.text()) == "false": + return "无该命令!" + except ClientConnectionError: + is_run = False + return f"{host}未发现jetbot,连接失败!!!" + + async def __aexit__(self, *args): + await self.session.close() + + +@cmd.handle() +async def cmd_(bot: Bot, event: MessageEvent): + text = event.get_plaintext() + if text: + async with JetBot(text) as res: + if "login" in text: + global host + host = text.split()[-1] + await cmd.finish(await res.get()) + else: + if is_run: + rep = await res.post() + if rep: + await cmd.finish(rep) + else: + await cmd.finish("未与机器人建立连接") diff --git a/src/plugins/weather/README.md b/src/plugins/weather/README.md new file mode 100644 index 0000000..895df19 --- /dev/null +++ b/src/plugins/weather/README.md @@ -0,0 +1,3 @@ +# 天气插件 + +命令: 天气 \ No newline at end of file diff --git a/src/plugins/weather/__init__.py b/src/plugins/weather/__init__.py new file mode 100644 index 0000000..169fad5 --- /dev/null +++ b/src/plugins/weather/__init__.py @@ -0,0 +1,31 @@ +from nonebot import require, on_command, get_bot, get_driver +from .data_source import GetCurrentWeather, config, ToDayWeather +from asyncio import sleep +from nonebot.adapters.cqhttp import Bot, MessageEvent + +weather = on_command("天气") +scheduler = require("nonebot_plugin_apscheduler").scheduler + + +@weather.handle() +async def _(bot: Bot, event: MessageEvent): + if not config.loading_weather: + await weather.send("正在获取当前天气,请稍后...") + async with GetCurrentWeather() as res: + await weather.finish(res.text()) + else: + await weather.finish("请勿重复请求天气!") + + +async def today_weather(bot=None): + bot: Bot = get_bot() + async with ToDayWeather() as res: + text = list(res.text()) + # for group_id in await res.get_class(): + # for msg in text: + # await sleep(1) + # await bot.send_group_msg(group_id=, message=msg) + +# get_driver().on_bot_connect(today_weather) + +scheduler.add_job(today_weather, 'cron', hour=6, minute=0, id='today_weather') diff --git a/src/plugins/weather/config.py b/src/plugins/weather/config.py new file mode 100644 index 0000000..6227671 --- /dev/null +++ b/src/plugins/weather/config.py @@ -0,0 +1,23 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + loading_weather = False + + # 中国天气气象局 + class CmaWeatherApi: + url = "https://weather.cma.cn/api/now/57679" + params = {} + + # oppo color 天气 + class OppoWeatherApi: + url = "https://weather.oppomobile.com/dailyForecastAd/v1/00282430113087" + params = { + "language": "zh-cn", + "unit": "c", + "appVerCode": "8002026", + "appVerName": "8.2.26", + "caller": "WEATHER_APP", + "frontCode": "2.0", + "fromWeatherApp": "true" + } diff --git a/src/plugins/weather/data_source.py b/src/plugins/weather/data_source.py new file mode 100644 index 0000000..ab3b4a0 --- /dev/null +++ b/src/plugins/weather/data_source.py @@ -0,0 +1,158 @@ +import re +from asyncio import sleep +from json import loads +from time import time +from aiohttp import ClientSession, ClientConnectionError +from nonebot.adapters.cqhttp import MessageSegment, Message +from nonebot import require +from .config import Config +from typing import Optional +from datetime import datetime + +config = Config() +weather_api = Config.OppoWeatherApi +mysql = require("botdb").MySQLdbMethods() + + +class MessageBar(Message): + def __getattr__(self, item): + return lambda *args, **kwargs: self.append(getattr(MessageSegment, item)(*args, **kwargs)) + + +class GetCurrentWeather: + weather_api = Config.CmaWeatherApi + """获取当时天气""" + async def close(self): + await self.session.close() + + def __init__(self): + config.loading_weather = True + self.data: Optional[dict] = None + + async def __aenter__(self): + self.session = ClientSession() + await self.get() + return self + + async def get(self): + async with self.session.get(self.weather_api.url, params=self.weather_api.params) as res: + self.data = (await res.json())["data"] + + def precipitation(self): + if self.data["now"]["precipitation"] >= 100: + return 0.0 + return self.data["now"]["precipitation"] + + def text(self): + config.loading_weather = False + location = self.data["location"] + now = self.data["now"] + return (MessageBar().text( + f"{location['name']}当前天气状况\n" + f"温度: {now['temperature']}°C\n" + f"湿度: {now['humidity']}%\n" + f"降雨: {now['precipitation']}mm\n" + f"风向: {now['windDirection']} {now['windScale']}\n" + f"大气压: {now['pressure']}hPa\n" + f"数据更新时间: {self.data['lastUpdate']}" + )) + + async def __aexit__(self, *args): + await self.session.close() + + +class ToDayWeather: + url = "https://wis.qq.com/weather/common" + direction = ["北风", "东北风", "东风", "东南风", "南风", "西南风", "西风", "西北风"] + + @property + def params(self): + return { + "source": "pc", + "weather_type": "observe|index|tips|air|forecast_24h", + "province": "湖南省", + "city": "长沙市", + "county": "", + "callback": "weather", + "_": time() + } + + def forecast(self): + day = str(self.date.date()) + forecast = self.data["forecast_24h"] + for i in forecast: + if forecast[i]["time"] == day: + return forecast[i]["max_degree"] + "-" + forecast[i]["min_degree"] + "°C" + + async def get(self, err_: int = 0) -> dict: + try: + async with ClientSession() as session: + async with session.get(self.url, params=self.params) as res: + return loads(re.sub(r"^\S+\(|\)$", "", await res.text()))["data"] + except ClientConnectionError as err: + if err_ < 3: + return await self.get(err_ + 1) + else: + raise ClientConnectionError(err) + + def suggest(self): + """今日天气建议""" + text = [] + index: dict = self.data.get("index") + for i in {"sports", + "clothes", + # "comfort", + # "dry", + "makeup", + "morning", + "umbrella", + # "allergy", + "ultraviolet" + # "sunscreen" + }: + value = index[i]["detail"] or index[i]["info"] + if value: + text.append(f'{index[i]["name"]}:{value}') + return "\n- - - - - - - - - - -\n".join(text) + + def weather(self): + """天气""" + w = self.data["observe"] + return (f"今天是{self.date.date()}长沙当前天气\n" + f"天气:{w['weather']}\n" + f"气温:{self.forecast()}\n" + f"当前气温:{w['degree']}°C\n" + f"气压:{w['pressure']}hPa\n" + f"湿度:{w['humidity']}%\n" + f"降雨率:{w['precipitation']}%\n" + f"风向:{self.direction[int(w['wind_direction'])]} {w['wind_power']}级") + + def air(self): + """空气质量""" + air = self.data["air"] + return (f"空气质量:{air['aqi']} {air['aqi_name']}\n" + f"co:{air['co']}\n" + f"no2:{air['no2']}\n" + f"o3:{air['o3']}\n" + f"pm10:{air['pm10']}\n" + f"pm2.5:{air['pm2.5']}\n" + f"so2:{air['so2']}" + ) + + def text(self): + yield self.weather() + "\n- - - - - - - - - -\n" + self.air() + yield self.suggest() + + @staticmethod + async def get_class(): + await mysql.execute("select * from class_table") + class_id = list(mysql.form().get("class_group")) + return class_id or [] + + async def __aenter__(self): + self.date = datetime.now() + self.data = await self.get() + return self + + async def __aexit__(self, *args): + ... diff --git a/src/utils/README.md b/src/utils/README.md new file mode 100644 index 0000000..804776d --- /dev/null +++ b/src/utils/README.md @@ -0,0 +1,12 @@ +# 功能插件 + +提高代码混淆度 + +功能 +- 数据库操作 +- permission +- rule +- 文件读取 + - 读取和写入数据,方便直接获取文件路径保存文件 +- 工具箱 + - 常用爬虫或功能集成 \ No newline at end of file diff --git a/src/utils/botdb/__init__.py b/src/utils/botdb/__init__.py new file mode 100644 index 0000000..49deb07 --- /dev/null +++ b/src/utils/botdb/__init__.py @@ -0,0 +1,6 @@ +from nonebot import export +from .data_source import MySQLdbMethods + +export = export() + +export.MySQLdbMethods = MySQLdbMethods diff --git a/src/utils/botdb/data_source.py b/src/utils/botdb/data_source.py new file mode 100644 index 0000000..9624264 --- /dev/null +++ b/src/utils/botdb/data_source.py @@ -0,0 +1,89 @@ +from aiomysql import connect, Warning, Connection, Cursor, IntegrityError, InternalError, OperationalError, Error +from nonebot import get_driver +from warnings import filterwarnings +from nonebot.log import logger +from pandas import DataFrame + +driver = get_driver() +filterwarnings("error", category=Warning) + + +class MySQLdbMethods: + __slots__ = ("_conn", "cur", "description") + cur: Cursor + is_run = False # 数据库是否启动 + _conn: Connection + config = { + "db": driver.config.mysql_db, + "host": driver.config.mysql_host, + "user": driver.config.mysql_user, + "password": str(driver.config.mysql_password), + } + + async def execute(self, query: str, param: list = None, err_num: int = 5): + try: + await self.cur.execute(query, param) + except (RuntimeError, InternalError) as err: + if err_num: + logger.warning("数据库连接超时,重新建立连接!") + await self.close() + await self.connect() + await self.execute(query, param, err_num-1) + else: + print(query, param) + raise err + except IntegrityError: + await self._conn.rollback() + raise IntegrityError + + async def execute_commit(self, query: str, param: list = None): + await self.execute(query, param) + await self._conn.commit() + + def fetchall(self): + return self.cur.fetchall() + + def form(self) -> DataFrame: + """将表格数据转为DataFrame""" + data = self.fetchall().result() + return DataFrame({t[0]: [v[i] for v in data] for i, t in enumerate(self.cur.description)}) + + @classmethod + async def connect(cls): + """与数据库建立连接""" + cls._conn = await connect(**cls.config) + cls.cur = await cls._conn.cursor() + + @classmethod + async def close(cls): + """关闭数据库""" + try: + await cls.cur.close() + cls._conn.close() + except Error as err: + logger.error("数据库断开出现错误") + logger.error(err) + + @classmethod + async def start(cls): + """开启数据库""" + try: + await cls.connect() + logger.success("成功与MySQL数据库建立连接!") + except AssertionError as err: + logger.error(err) + except OperationalError as err: + logger.error("数据库连接失败!请检查配置文件是否输入有误!") + logger.error(err) + else: + cls.is_run = True + + @classmethod + async def stop(cls): + """停止数据库""" + if cls.is_run: + await cls.close() + + +driver.on_startup(MySQLdbMethods.start) +driver.on_shutdown(MySQLdbMethods.stop) diff --git a/src/utils/permission/__init__.py b/src/utils/permission/__init__.py new file mode 100644 index 0000000..f58449b --- /dev/null +++ b/src/utils/permission/__init__.py @@ -0,0 +1,40 @@ +from nonebot import require, export +from nonebot.permission import Permission, SUPERUSER +from nonebot.adapters.cqhttp import Bot, Event, MessageEvent + +MySQLdbMethods = require("botdb").MySQLdbMethods +mysql = MySQLdbMethods() +export = export() + + +async def _teacher(bot: Bot, event: Event): + """分析是否为教师或超级用户,如果是则满足条件""" + if event.get_type() == "message": + event: MessageEvent + user_id = event.get_user_id() + # 是否为超级用户 + if user_id in bot.config.superusers: + return True + else: + # 是否为教师 + await mysql.execute("select qq from teacher where qq=%s" % event.user_id) + return event.user_id in set(mysql.form()["qq"]) + return False + + +async def _class_group(bot: Bot, event: Event): + if event.get_type() == "message": + event: MessageEvent + user_id = event.get_user_id() + # 是否为超级用户 + if user_id in bot.config.superusers: + return True + else: + await mysql.execute("select qq from user_info union select qq from teacher") + return event.user_id in set(mysql.form()["qq"]) + return False + +CLASS_GROUP = Permission(_class_group) +TEACHER = Permission(_teacher) +export.CLASS_GROUP = CLASS_GROUP +export.TEACHER = TEACHER diff --git a/src/utils/readfile/__init__.py b/src/utils/readfile/__init__.py new file mode 100644 index 0000000..744854d --- /dev/null +++ b/src/utils/readfile/__init__.py @@ -0,0 +1,63 @@ +from aiofile import async_open +from nonebot import export +from os.path import join, dirname +import os + +export = export() + +global_default_path: str = os.getcwd() + + +def set_path(path): + """设置读取的文件路径""" + global global_default_path + global_default_path = path + + +class ReadFile: + # 默认路径 + default_path: str = global_default_path + + def __init__(self, *args: str): + """ + :param args: 默认路径下的其它文件 + """ + self.path = join(self.default_path, *args) + + async def read(self, file_name, *, mode="r"): + async with async_open(join(self.path, file_name), mode=mode) as file: + return await file.read() + + async def write(self, file_name, data, *, mode="w", end=True): + try: + async with async_open(join(self.path, file_name), mode=mode) as file: + return await file.write(data) + except FileNotFoundError as err: + if end: + self.mkdir(dirname(file_name)) + await self.write(file_name, data, mode=mode, end=False) + else: + raise err + + def listdir(self, *args: str) -> list: + return os.listdir(join(self.path, *args)) + + def remove(self, *args: str) -> bool: + try: + os.remove(join(self.path, *args)) + return True + except FileNotFoundError: + return False + + def mkdir(self, *args: str) -> bool: + try: + os.makedirs(join(self.path, *args)) + return True + except FileExistsError: + return False + + +# 设置读取的文件路径 +export.set_path = set_path +export.ReadFile = ReadFile + diff --git a/src/utils/readfile/config.py b/src/utils/readfile/config.py new file mode 100644 index 0000000..6b74c37 --- /dev/null +++ b/src/utils/readfile/config.py @@ -0,0 +1,11 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + # Your Config Here + dir_name = "src" + data_dir_name = "data" + config_dir_name = "config" + + class Config: + extra = "ignore" diff --git a/src/utils/readfile/data_source.py b/src/utils/readfile/data_source.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/rule/__init__.py b/src/utils/rule/__init__.py new file mode 100644 index 0000000..a574ebc --- /dev/null +++ b/src/utils/rule/__init__.py @@ -0,0 +1,105 @@ +from nonebot import require, export +from nonebot.exception import FinishedException +from pandas import DataFrame +from nonebot.rule import T_State, Rule +from .config import Config +from nonebot.adapters.cqhttp import Bot, Event, GroupMessageEvent, MessageEvent + +MySQLdbMethods = require("botdb").MySQLdbMethods +mysql = MySQLdbMethods() +config = Config() +export = export() + + +# 是否为班主任 +async def _master(bot: Bot, event: Event, state: T_State, is_raise: bool = True): + """分析是否是班主任""" + if event.get_type() == "message": + event: GroupMessageEvent + await mysql.execute("select class_group, class_name from class_table where class_teacher=%s" % event.user_id) + data = mysql.form() + state["all_group"] = { + "class_group": list(data["class_group"]), + "class_name": list(data["class_name"]) + } + if event.group_id in state["all_group"]["class_group"]: + return True + if is_raise: + raise FinishedException + return False + + +# 是否为班干部 +async def _class_cadre(bot: Bot, event: Event, state: T_State, is_raise: bool = True): + """ + 分析是否为班干部 + 会返回用户的姓名,班级,职位到state内 + """ + if event.get_type() == "message": + event: MessageEvent + await mysql.execute("select 姓名, 职位, 班级 from user_info where qq=%s" % event.user_id) + data = mysql.form() + if data.shape[0]: + state["class_cadre"] = data.loc[0] + if state["class_cadre"]["职位"] in config.class_cadre: + return True + if is_raise: + raise FinishedException + return False + + +# 是否为学生 +async def _is_user(bot: Bot, event: Event, state: T_State, is_raise: bool = True): + """ + 分析是否为班级学生,并且返回学生信息(姓名,职位,班级,学号) + """ + event_type = event.get_type() + if event_type == "message" or event_type == "request": + event: MessageEvent + await mysql.execute("select 姓名, 职位, 班级, 学号, 分数 from user_info where qq=%s" % event.user_id) + data: DataFrame = mysql.form() + if data.shape[0]: + state["user_info"] = data.loc[0] + return True + if is_raise: + raise FinishedException + return False + + +# 是否为班级群 +async def _class_group(bot: Bot, event: Event, state: T_State, is_raise: bool = True): + event_type = event.get_type() + if event_type == "message": + event: GroupMessageEvent + await mysql.execute("select * from class_table where class_group=%s", [event.group_id]) + data: DataFrame = mysql.form() + if data.shape[0]: + state["class_group"] = data.loc[0] + return True + if is_raise: + raise FinishedException + return False + + +# 是否为寝室长 +async def _dorm_admin(bot: Bot, event: Event, state: T_State, is_raise: bool = True): + event_type = event.get_type() + if event_type == "message": + event: GroupMessageEvent + await mysql.execute("select * from user_info where 寝室长='是' and qq=%s", [event.user_id]) + data: DataFrame = mysql.form() + if data.shape[0]: + state["dorm_admin"] = data.loc[0] + return True + if is_raise: + raise FinishedException + return False + + +export.DORM_ADMIN = Rule(_dorm_admin) +export.MASTER = Rule(_master) +export.CLASS_GROUP = Rule(_class_group) +export.IS_USER = Rule(_is_user) +export.CLASS_CADRE = Rule(_class_cadre) +export.cadres = config.class_cadre + diff --git a/src/utils/rule/config.py b/src/utils/rule/config.py new file mode 100644 index 0000000..f31fee7 --- /dev/null +++ b/src/utils/rule/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + class_cadre = ["组织委员", "心理委员", "宣传委员", "体育委员", "男生委员", + "女生委员", "治保委员", "副班长", "学习委员", "生卫委员", + "权益委员", "班长", "团支书"] diff --git a/src/utils/tools/__init__.py b/src/utils/tools/__init__.py new file mode 100644 index 0000000..b2991d9 --- /dev/null +++ b/src/utils/tools/__init__.py @@ -0,0 +1,32 @@ +import re +from nonebot import export +from nonebot.adapters.cqhttp import Message +from .data_source import GetDocsSheet +from .config import Config +from .watermark import Watermark +from typing import Union + + +def re_docs(text: str) -> str: + return re.search(r"https://docs.qq.com/sheet/\S[^\"']+", str(text).replace("\\", ""), re.I).group() + + +def get_url(message: Union[Message, str]) -> str: + url = None + if isinstance(message, Message): + for msg in message: + if msg.type == "json" or msg.type == "xml": + url = re_docs(msg.data["data"]) + elif msg.type == "text": + url = re_docs(msg.data["text"]) + if url: + return url + else: + return re_docs(message) + + +export = export() +export.GetDocsSheet = GetDocsSheet +export.Watermark = Watermark +export.get_url = get_url +export.re_docs = re_docs diff --git a/src/utils/tools/config.py b/src/utils/tools/config.py new file mode 100644 index 0000000..0ef390f --- /dev/null +++ b/src/utils/tools/config.py @@ -0,0 +1,8 @@ +from pydantic import BaseSettings + + +class Config(BaseSettings): + # Your Config Here + + class Config: + extra = "ignore" \ No newline at end of file diff --git a/src/utils/tools/data_source.py b/src/utils/tools/data_source.py new file mode 100644 index 0000000..2eac004 --- /dev/null +++ b/src/utils/tools/data_source.py @@ -0,0 +1,153 @@ +from pandas import DataFrame +from aiohttp import ClientSession +from datetime import datetime, timedelta +import re + +from typing import List + +docs_url = "https://docs.qq.com/dop-api/opendoc" +headers = { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/94.0.4606.61 Safari/537.36 ", + "Upgrade-Insecure-Requests": "1", + "Cache-Control": "max-age=0", + "Connection": "keep-alive", + "appName": "Opera" +} + + +class GetDocsSheet: + + def __init__(self, url: str): + self.url = url + self.session = ClientSession() + + @property + def params(self): + """提取URL所需params""" + params = { + "outformat": 1, + "normal": 1 + } + url = self.url.split("/")[-1] + param = re.findall(r"^\w+|tab=\w+", url) + params["id"] = param[0] + if len(param) > 1: + params["tab"] = param[1].replace("tab=", "") + return params + + async def get(self): + async with self.session.get(docs_url, headers=headers, params=self.params) as res: + return await res.json() + + @staticmethod + def extract(data: dict): + for values in data["clientVars"]["collab_client_vars"]["initialAttributedText"]["text"][0]: + for value in values: + if isinstance(value["c"][1], dict): + for i in value: + if isinstance(value[i], list): + return value[i] + + def convert2d(self) -> list: + """ + 转换未二维列表 + + 从列表第一位开始取,按照长度推算出后面内容的序号 + 然后按照序号去循环,推导出内容 + 如果序号存在就查看序号内是否有值,取出值 + 不存在则全部替换为空 + """ + data = [] + keys = list(self.raw_data.keys()) + length = len(keys) + index = 0 + while index < length: + key = int(keys[index]) + if not key % self.columns: + arr = [] + for key in range(key, key + self.columns): + values = self.raw_data.get(str(key)) + if values: + index += 1 + value = values.get("2") + arr.append(value[1] if value else value) + continue + arr.append(values) + data.append(arr) + else: + index += 1 + return data + + def data_frame(self): + """ + 找出columns所在位置 + """ + # 清除空行空列 + df = DataFrame(self.convert2d(), dtype="str").dropna(axis=0, how='all').dropna(axis=1, how='all') + for i, v in enumerate(df.values): + if all(v): + return DataFrame(df.values[i+1:], columns=list(v)) + + async def __aenter__(self): + data = self.extract(await self.get()) + self.columns = data[0][-1] + 1 + self.rows = data[0][-3] + 1 + self.raw_data = data[1] + self.data = self.data_frame() + return self + + @staticmethod + def get_excel_date(date_int: int): + """对于excel日期进行转换,以便写入数据""" + if date_int: + return str((datetime(1899, 12, 30) + timedelta(int(date_int) + 1)).date()) + return None + + def reset_date(self, typeof: str = "出生日期"): + """重置表格日期表格""" + try: + self.data[typeof] = [self.get_excel_date(i) for i in self.data[typeof]] + except IndexError: + ... + finally: + return self + + async def __aexit__(self, *args): + await self.session.close() + + +class QueryUser: + def __init__(self, params: List[str], class_name: str = None, group_id: int = None): + if not(class_name or group_id): + raise "Missing required parameters class_name or group_id" + self.params = params + self.class_name = class_name + self.group_id = group_id + self.index = [] + self.name = [] + self.user_id = [] + + def split_param(self): + """ + 拆分参数 + 字符串作为名字传入 name + 如果是数字判断数字是序号还是id + 满足长度四位的为序号 + 超过的为id + """ + for i in self.params: + if i.isdigit(): + i = int(i) + if i < 1000: + self.index.append(i) + else: + self.user_id.append(i) + else: + self.name.append(i) + + async def __aenter__(self): + ... + + async def __aexit__(self, *args): + ... diff --git a/src/utils/tools/watermark.py b/src/utils/tools/watermark.py new file mode 100644 index 0000000..8c1ab53 --- /dev/null +++ b/src/utils/tools/watermark.py @@ -0,0 +1,62 @@ +from typing import Tuple, Union + +from PIL import Image, ImageDraw, ImageFont +from PIL.PngImagePlugin import PngImageFile +from aiohttp import ClientSession +from io import BytesIO + + +class Watermark: + def __init__(self, + file: Union[str, bytes] = None, + url: str = None, + text: str = None, + size: int = 50, + font: str = "simhei.ttf", + xy: Tuple[int, int] = None, + fill: Tuple[int, int, int] = None): + self.url = url + self.file = file + self.text = text + self.size = size + self.font = font + self.xy = xy or (0, 0) + self.fill = fill or (255, 0, 0) + + async def url_img(self) -> PngImageFile: + """下载图片""" + async with ClientSession() as session: + async with session.get(self.url) as res: + return Image.open(BytesIO(await res.read())) + + def byte_img(self) -> PngImageFile: + """数据流""" + return Image.open(BytesIO(self.file)) + + def local_img(self): + """本地图片""" + return Image.open(self.file) + + def draw(self, img: PngImageFile = None) -> PngImageFile: + """绘制""" + img = self.img + draw = ImageDraw.Draw(img) + draw.text(xy=self.xy, text=self.text, fill=self.fill, + font=ImageFont.truetype(font=self.font, size=self.size)) + return img + + async def __aenter__(self): + if self.file: + if isinstance(self.file, bytes): + self.img = self.byte_img() + else: + self.img = self.local_img() + elif self.url: + self.img = await self.url_img() + else: + raise TypeError("String or Bytes") + self.draw() + return self + + async def __aexit__(self, *args): + ...