├── LICENSE ├── README.md ├── __init__.py ├── resources ├── 2022-08-22-23-11-43.png ├── 2022-08-22-23-19-00.png ├── 2022-08-22-23-20-19.png ├── 2022-08-22-23-21-31.png ├── 2022-08-22-23-22-17.png ├── 2022-08-22-23-23-40.png ├── 2022-08-22-23-33-19.png ├── 2022-08-23-11-57-14.png └── UJP@BUK%TOA95TWWL97}}KT.jpg ├── template.py └── utils.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 InariInDream 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | nonebot 3 |

4 |
5 | 6 | # nonebot-plugin-course 7 | 8 | ✨*基于Nonebot2的课表查询插件*✨ 9 | 10 |
11 |
12 | python 13 | 14 | license 15 | 16 | 17 | pypi 18 | 19 | 20 | nonebot2 21 | 22 | 23 | pypi download 24 | 25 | 26 | 27 |
28 | 29 | 30 | [//]: # () 31 | 32 | [//]: # (* [nonebot-plugin-course](#nonebot-plugin-course)) 33 | 34 | [//]: # ( * [💡写在前面](#写在前面)) 35 | 36 | [//]: # ( * [🌟功能](#功能)) 37 | 38 | [//]: # ( * [🎯TOD](#tod)) 39 | 40 | [//]: # ( * [暂时不会考虑添加的功能](#暂时不会考虑添加的功能)) 41 | 42 | [//]: # ( * [💿如何使用](#如何使用)) 43 | 44 | [//]: # ( * [下载](#下载)) 45 | 46 | [//]: # ( * [方法一](#方法一)) 47 | 48 | [//]: # ( * [方法二](#方法二)) 49 | 50 | [//]: # ( * [方法三](#方法三)) 51 | 52 | [//]: # ( * [初次使用](#初次使用)) 53 | 54 | [//]: # ( * [示例](#示例)) 55 | 56 | [//]: # ( * [注意事项](#注意事项)) 57 | 58 | [//]: # ( * [📈功能展示](#功能展示)) 59 | 60 | [//]: # ( * [指令](#指令)) 61 | 62 | [//]: # ( * [感谢你看到这里,如果觉得还不错的话给个star吧](#感谢你看到这里如果觉得还不错的话给个star吧)) 63 | 64 | [//]: # () 65 | [//]: # () 66 | 67 | [//]: # () 68 | 69 | [//]: # () 70 | [//]: # () 71 | 72 | 73 | 74 | 75 | ## 💡写在前面 76 | 77 | 特别感谢[hamo](https://github.com/hamo-reid)的[*nonebot_plugin_PicMenu*](https://github.com/hamo-reid/nonebot_plugin_PicMenu)提供的图片生成工具img_tool和很多思路 78 | 79 | 算是菜鸡写的第一个完整的工程项目,代码似乎十分屎山,但作为总计写python不到两个月的人来说,我还是较为有成就感的。之后有空再重构,所以有什么改进建议或者在使用时遇到bug,欢迎狠狠地提Issue 80 | 81 | ## 更新记录 82 | 83 | - 2022.8.28:支持不同账号设定不同的上下课时间,现在可在json文件里自动生成的"exact_time"下更改 84 | - 2022.9.9:修复编码问题,此前在json里保存的课程名会出现乱码 85 | - 2022.9.28:修复第13节课不能正常显示的问题(前两周没第13节课都没发现) 86 | - 2022.9.28:新增查询第二天有无早八功能 87 | - 2022.10.17:修复在没有data文件夹时的创建文件夹问题 88 | - 2023.2.13:新增自定义一周上课天数和每天上课节数功能 89 | 90 | 91 | ## 🌟功能 92 | 93 | * 📖完整课表 94 | * 📙本周课表 95 | * 🧾下周课表 96 | * 🔍查询指定周数课表 97 | * 🕑查询当前在上什么课,今天还有没有课 98 | * 📆支持设定周数 99 | * 📌添加课表过程简单,与主流课表app流程无太大区别 100 | 101 | ### 🎯TODO 102 | 103 | ☑︎ 支持不同账号设定不同的上下课时间,而非一个bot共用一套上下课时间 104 | 105 | ☑︎ 查询明天有无早八,好决定今晚熬不熬夜 106 | 107 | ☑︎ 自定义上课的天数和每日的节数 108 | 109 | ⬜︎ 如果今天没课了,跨天或跨周查询离现在最近的一节课还有多久(课少的时候看着很赏心悦目 110 | 111 | ⬜︎ 相同课的表格合并 112 | 113 | ⬜︎ 这个老师经常点名?在Ta上课前半小时自动提醒我 114 | 115 | 116 | 117 | 118 | ### ❎暂时不会考虑添加的功能 119 | - 用对话添加课表:因为一周内的课十分的多,多的时候会添加5×13次,而这都需要一次一次地在聊天框输入后发送,若遇传参问题会增加复杂度,反而本末倒置了。若是在群组里添加也会十分地刷屏。 120 | 121 | 122 | ## 💿如何使用 123 | 124 | - 首先部署nonebot,具体可参照[这里](https://v2.nonebot.dev/docs/start/installation) 125 | 126 | ### 下载 127 | #### 方法一 128 | 129 | ``` 130 | nb plugin install nonebot-plugin-course 131 | ``` 132 | 133 | #### 方法二 134 | - Step1 135 | ``` 136 | pip install nonebot_plugin_course 137 | ``` 138 | - Step2 139 | 在pyproject.toml里的`plugins = []`添加`"nonebot_plugin_course"` 140 | 141 | #### 方法三 142 | 143 | 在src文件夹下的plugin文件夹里使用`git clone` 144 | 145 | ### 初次使用 146 | 1. 上一步完成以后启动一次bot,会自动在根目录的/data文件夹(即bot.py同级文件夹)下生成一个名为course_config的文件夹 147 | 148 | [//]: # () 149 | [//]: # ( #### **注意**:请确保自己的bot.py同级的文件夹下有一个名为data的文件夹,若没有请自行创建) 150 | 151 | [//]: # ( 文件结构应为如下所示:) 152 | 153 | [//]: # ( ```) 154 | 155 | [//]: # ( └─ YourBotName) 156 | 157 | [//]: # ( │ .env) 158 | 159 | [//]: # ( │ .env.dev) 160 | 161 | [//]: # ( │ .env.prod) 162 | 163 | [//]: # ( │ .gitignore) 164 | 165 | [//]: # ( │ bot.py) 166 | 167 | [//]: # ( │ docker-compose.yml) 168 | 169 | [//]: # ( │ Dockerfile) 170 | 171 | [//]: # ( │ pyproject.toml) 172 | 173 | [//]: # ( │ README.md) 174 | 175 | [//]: # ( │) 176 | 177 | [//]: # ( ├─ __pycache__) 178 | 179 | [//]: # ( ├─ src) 180 | 181 | [//]: # ( └─ data # 自创的名为 data 的文件夹) 182 | 183 | [//]: # ( └─ course_config # (将会由插件自动生成的名为 course_config 的文件夹)) 184 | 185 | [//]: # ( ```) 186 | 187 | 188 | 2. 打开里面的config.json文件,将"default"字段的值改为任意字体的路径,字体格式为[PIL.ImageFont.truetype](https://pillow.readthedocs.io/en/stable/reference/ImageFont.html?highlight=truetype#PIL.ImageFont.truetype)所支持的字体,比如将第一行改为`"default": "simhei.ttf"` 189 | 190 | 3. 保存json文件后就可以快乐地添加课表了 191 | 192 | 4. 在任意一个群内发送“完整课表”,即可自动初始化当前帐号的课表结构 193 | 194 | ![](https://github.com/InariInDream/nonebot_plugin_course/blob/main/resources/2022-08-22-23-33-19.png) 195 | 196 | ![](https://github.com/InariInDream/nonebot_plugin_course/blob/main/resources/2022-08-22-23-11-43.png) 197 | 198 | 5. 根据自己的课表情况在框内填写即可 199 | 200 | #### 示例 201 | ```json 202 | "qq号": { 203 | "week": 1, 204 | "exact_time": {//自定义用户每节课的上下课时间 205 | "1": { 206 | "start": "08:20", 207 | "end": "09:05" 208 | }, 209 | "2": { 210 | "start": "09:10", 211 | "end": "09:55" 212 | }, 213 | ... 214 | }, 215 | "1": { // 周一 216 | "1": [ // 第一节课 217 | { 218 | "name": "", 219 | "teacher": "", 220 | "classroom": "", 221 | "week": [] 222 | } 223 | ], 224 | "2": [ // 第二节课 225 | { 226 | "name": "", 227 | "teacher": "", 228 | "classroom": "", 229 | "week": [] 230 | } 231 | ], 232 | "3": [ //注意如果某天的某个位置在不同周有多节课,在列表位置以相同格式复制字典后再填写即可 233 | { 234 | "name": "数字图像处理基础", 235 | "teacher": "虞旦", 236 | "classroom": "物流113", 237 | "week": [ 238 | 8 239 | ] 240 | }, 241 | { 242 | "name": "数字图像处理基础", 243 | "teacher": "王晓兰", 244 | "classroom": "物流113", 245 | "week": [ // 周数需一个一个填,暂不支持如"3-13"的形式填写 246 | 9, 247 | 10, 248 | 11, 249 | 12, 250 | 13, 251 | 15, 252 | 16 253 | ] 254 | } 255 | ], 256 | ... 257 | }, 258 | "2":{ // 周二 259 | ... 260 | }, 261 | ... 262 | } 263 | ``` 264 | 265 | ## ❗**注意事项** 266 | 267 | 1. **不同学校的上课时间有所不同**,默认设定的时间仅代表作者的学校。因此,请**务必**确认utils.py里默认初始化的上下课时间是否与使用者的一致 268 | ```py 269 | self.data_manager.course_data[user_id] = {'week': 1, 270 | 'exact_time': { 271 | "1": {"start": "08:20", "end": "09:05"}, 272 | "2": {"start": "09:10", "end": "09:55"}, 273 | "3": {"start": "10:15", "end": "11:00"}, 274 | "4": {"start": "11:05", "end": "11:50"}, 275 | "5": {"start": "11:55", "end": "12:25"}, 276 | "6": {"start": "12:30", "end": "13:00"}, 277 | "7": {"start": "13:10", "end": "13:55"}, 278 | "8": {"start": "14:00", "end": "14:45"}, 279 | "9": {"start": "15:05", "end": "15:50"}, 280 | "10": {"start": "15:55", "end": "16:40"}, 281 | "11": {"start": "18:00", "end": "18:45"}, 282 | "12": {"start": "18:50", "end": "19:35"}, 283 | "13": {"start": "19:40", "end": "20:25"}, 284 | }, 285 | } 286 | ``` 287 | 288 | 2. 如果某一节课在不同周有不同的教室,建议在填写`classroom`的时候就以原始形式填写 289 | ``` 290 | "classroom": "2-3:3B403,4-6:2A401" 291 | ``` 292 | 因为除了“完整课表”以外的查询命令,都会返回查询者的当前周数。(若在填写时以字典形式存每周对应的教室反而会增加填写难度和出错率。) 293 | 294 | 3. 参数说明(在全局配置文件里修改) 295 | 296 | | 参数名 | 类型 | 释义 | 默认值 | 297 | | ----------------------------- | ----- | ------------------------------------------ | ------------------- | 298 | | column_num | int | 每周上课天数 | 7 | 299 | | row_num | int | 每日上课最大节数 | 13 | 300 | 301 | ## 📈功能展示 302 | 303 | ### 指令 304 | 305 | - **本周课表**:查看这周的课表 306 | - **完整课表**:查看完整的课表 307 | - **下周课表**:查看下周的课表 308 | - **查看课表** + **周数**:查询指定周的课表 309 | - **设置周数** + **周数**:设定当前是第几周 310 | - **上课**:查询当前是否有课,及今天的下一节课是什么,还有多久上 311 | - **明日早八**:查询明天是否有早八 312 | 313 | 314 | ![](https://github.com/InariInDream/nonebot_plugin_course/blob/main/resources/2022-08-22-23-20-19.png) 315 | 316 | ![](https://github.com/InariInDream/nonebot_plugin_course/blob/main/resources/2022-08-22-23-22-17.png) 317 | 318 | ![](https://github.com/InariInDream/nonebot_plugin_course/blob/main/resources/2022-08-23-11-57-14.png) 319 | 320 | ![](https://github.com/InariInDream/nonebot_plugin_course/blob/main/resources/UJP%40BUK%25TOA95TWWL97%7D%7DKT.jpg) 321 | 322 | ![](https://github.com/InariInDream/nonebot_plugin_course/blob/main/resources/2022-08-22-23-23-40.png) 323 | 324 | 325 | 326 | 327 | ## 感谢你看到这里,如果觉得还不错的话给个star吧 328 | 329 | 330 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from nonebot import logger 2 | from nonebot.params import CommandArg 3 | 4 | from nonebot import on_startswith, on_command, require 5 | from nonebot.adapters.onebot.v11 import ( 6 | GROUP, 7 | GroupMessageEvent, 8 | MessageSegment, 9 | Message 10 | ) 11 | 12 | from PIL import Image 13 | 14 | from .utils import course_manager, get_weekday 15 | from nonebot_plugin_PicMenu.img_tool import img2b64 16 | from nonebot.plugin import PluginMetadata 17 | 18 | __plugin_meta__ = PluginMetadata( 19 | name='课表查询', 20 | description="查询课表,今天还有什么课", 21 | usage=f"""本周课表:查看这周的课表\n 22 | 完整课表:查看完整的课表\n 23 | 下周课表:查看下周的课表\n 24 | 查看课表 + 周数:查询指定周的课表\n 25 | 设置周数 + 周数:设定当前是第几周\n 26 | 上课:查询当前是否有课,及今天的下一节课是什么,还有多久上""".strip(), 27 | ) 28 | 29 | scheduler = require("nonebot_plugin_apscheduler").scheduler 30 | 31 | check_full_timetable = on_startswith("完整课表", permission=GROUP, priority=5, block=False) 32 | 33 | week_timetable = on_startswith("本周课表", permission=GROUP, priority=5, block=True) 34 | 35 | next_week_timetable = on_startswith("下周课表", permission=GROUP, priority=5, block=True) 36 | 37 | now_course = on_command("上课", permission=GROUP, priority=5, block=True) 38 | 39 | set_now_week = on_command("设置周数", permission=GROUP, priority=5, block=True) 40 | 41 | check_timetable = on_command("查看课表", permission=GROUP, priority=5, block=True) 42 | 43 | tomo_course = on_command("明日早八", permission=GROUP, priority=5, block=True) 44 | 45 | 46 | # 每周一凌晨定时更新当前周数 47 | @scheduler.scheduled_job("cron", hour=1, minute=25) 48 | async def auto_update_current_week(): 49 | if get_weekday() == 1: 50 | course_manager.auto_update_week() 51 | logger.info("更新周数成功") 52 | 53 | 54 | @check_full_timetable.handle() 55 | async def _(event: GroupMessageEvent): 56 | img = course_manager.generate_timetable_image(event) 57 | if not isinstance(img, Image.Image): 58 | await check_full_timetable.finish(img) 59 | else: 60 | await check_full_timetable.finish(MessageSegment.image('base64://' + img2b64(img))) 61 | 62 | 63 | @week_timetable.handle() 64 | async def _(event: GroupMessageEvent): 65 | img = course_manager.generate_week_image(event, week=course_manager.get_week(event)) 66 | if not isinstance(img, Image.Image): 67 | await week_timetable.finish(img) 68 | else: 69 | await week_timetable.finish(MessageSegment.image('base64://' + img2b64(img))) 70 | 71 | 72 | @next_week_timetable.handle() 73 | async def _(event: GroupMessageEvent): 74 | img = course_manager.generate_week_image(event, week=course_manager.get_week(event) + 1) 75 | if not isinstance(img, Image.Image): 76 | await next_week_timetable.finish(img) 77 | else: 78 | await next_week_timetable.finish(MessageSegment.image('base64://' + img2b64(img))) 79 | 80 | 81 | @now_course.handle() 82 | async def _(event: GroupMessageEvent, arg: Message = CommandArg()): 83 | msg = course_manager.now_course(event) 84 | args = arg.extract_plain_text().strip() 85 | if args == "": 86 | await now_course.finish(msg) 87 | 88 | 89 | @tomo_course.handle() 90 | async def _(event: GroupMessageEvent, arg: Message = CommandArg()): 91 | msg = course_manager.tomo_course(event) 92 | args = arg.extract_plain_text().strip() 93 | if args == "": 94 | await tomo_course.finish(msg) 95 | 96 | 97 | @set_now_week.handle() 98 | async def _(event: GroupMessageEvent, arg: Message = CommandArg()): 99 | user_id = str(event.user_id) 100 | args = arg.extract_plain_text().strip() 101 | if args == "": 102 | await check_timetable.finish("请输入周数") 103 | if int(args) < 1: 104 | await set_now_week.finish("设置周数不能小于1") 105 | else: 106 | course_manager.set_week(event, int(args)) 107 | await set_now_week.finish(f"设置成功,{user_id}当前周数{args}") 108 | 109 | 110 | @check_timetable.handle() 111 | async def _(event: GroupMessageEvent, arg: Message = CommandArg()): 112 | args = arg.extract_plain_text().strip() 113 | if args == "": 114 | await check_timetable.finish("请输入周数") 115 | else: 116 | week = int(args) 117 | if week < 1: 118 | await check_timetable.finish("周数不能小于1") 119 | img = course_manager.generate_week_image(event, week) 120 | if not isinstance(img, Image.Image): 121 | await check_timetable.finish(img) 122 | else: 123 | await check_timetable.finish(MessageSegment.image('base64://' + img2b64(img))) 124 | 125 | 126 | -------------------------------------------------------------------------------- /resources/2022-08-22-23-11-43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/2022-08-22-23-11-43.png -------------------------------------------------------------------------------- /resources/2022-08-22-23-19-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/2022-08-22-23-19-00.png -------------------------------------------------------------------------------- /resources/2022-08-22-23-20-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/2022-08-22-23-20-19.png -------------------------------------------------------------------------------- /resources/2022-08-22-23-21-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/2022-08-22-23-21-31.png -------------------------------------------------------------------------------- /resources/2022-08-22-23-22-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/2022-08-22-23-22-17.png -------------------------------------------------------------------------------- /resources/2022-08-22-23-23-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/2022-08-22-23-23-40.png -------------------------------------------------------------------------------- /resources/2022-08-22-23-33-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/2022-08-22-23-33-19.png -------------------------------------------------------------------------------- /resources/2022-08-23-11-57-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/2022-08-23-11-57-14.png -------------------------------------------------------------------------------- /resources/UJP@BUK%TOA95TWWL97}}KT.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InariInDream/nonebot_plugin_course/dc11694746b939dd7c427884871eb19770eebff9/resources/UJP@BUK%TOA95TWWL97}}KT.jpg -------------------------------------------------------------------------------- /template.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import json 3 | from pathlib import Path 4 | from PIL import Image 5 | from pydantic import error_wrappers 6 | 7 | from nonebot_plugin_PicMenu.img_tool import simple_text, multi_text, ImageFactory, auto_resize_text 8 | from nonebot import logger, get_driver 9 | 10 | 11 | class DataManager(object): 12 | def __init__(self): 13 | """ 14 | 说明:初始化 15 | """ 16 | self.course_data = {} 17 | 18 | def load_class_info(self): 19 | """ 20 | 说明:加载课程信息 21 | :return: 22 | """ 23 | def load_from_json(_json_path: Path): 24 | """ 25 | 说明:加载json文件 26 | :param _json_path: 27 | :return: 28 | """ 29 | self.course_data = json.loads(_json_path.read_text(encoding='utf-8')) 30 | 31 | try: 32 | load_from_json(Path.cwd() / 'data' / 'course_config' / 'config.json') 33 | logger.success(f'课表数据加载成功') 34 | except json.JSONDecodeError as e: 35 | logger.opt(colors=True).error(f'课表 课表数据加载失败 (from json)\n' 36 | f'json解析失败: {e}') 37 | except error_wrappers.ValidationError as e: 38 | logger.opt(colors=True).error(f'课表 课表数据加载失败 (from json)\n' 39 | f'json缺少必要键值对: \n' 40 | f'{e}') 41 | return self.course_data 42 | 43 | 44 | class PicTemplate(metaclass=abc.ABCMeta): # 模板类 45 | def __init__(self): 46 | pass 47 | 48 | @abc.abstractmethod 49 | def generate_main_menu(self, data, event) -> Image: 50 | """ 51 | 生成主菜单抽象方法 52 | :param data: 53 | :param event: 54 | :return: 55 | """ 56 | pass 57 | 58 | 59 | class DefaultTemplate(PicTemplate): 60 | def __init__(self): 61 | super().__init__() 62 | self.name = 'default' 63 | self.load_resource() 64 | # 颜色 65 | self.colors = { 66 | 'blue': (23, 43, 59), 67 | 'yellow': (224, 164, 25), 68 | 'white': (237, 239, 241) 69 | } 70 | self.basic_font_size = 25 71 | self.current_week = None 72 | 73 | def load_resource(self): 74 | cwd = Path.cwd() 75 | with (cwd / 'data' / 'course_config' / 'config.json').open('r', encoding='utf-8') as fp: 76 | config = json.load(fp) 77 | self.using_font = config['default'] 78 | 79 | def generate_main_menu(self, data: dict, event, current_week=None) -> Image: 80 | user_id = event.user_id 81 | 82 | data_manager = DataManager() 83 | 84 | user_data = data_manager.load_class_info()[f"{user_id}"] 85 | # 列数 86 | try: 87 | # 读取配置文件 88 | column_num = get_driver().config.column_num 89 | except : 90 | column_num = 7 91 | 92 | # 行数 93 | try: 94 | # 读取配置文件 95 | row_num = get_driver().config.row_num 96 | except: 97 | row_num = 13 98 | 99 | # 数据及表头尺寸测算 100 | row_size_list = [[[300, 0]for _ in range(row_num + 1)] for _ in range(column_num + 1)] 101 | 102 | # 计算周一至周日课表的尺寸 103 | row_max = -1 104 | for x in range(column_num + 1): 105 | if x == 0: 106 | continue 107 | for y in range(row_num + 1): 108 | if y == 0: 109 | continue 110 | x = str(x) 111 | y = str(y) 112 | info = "" 113 | for course in data[x][y]: 114 | if current_week is not None and current_week not in course['week']: 115 | continue 116 | else: 117 | info += f"""{course["name"]}\n{course["teacher"]}\n{course["classroom"]}""" 118 | course_info_size = multi_text(info, 119 | default_font=self.using_font, 120 | default_size=25, 121 | box_size=(215, 0) 122 | ).size 123 | # 行高度计算 124 | x = int(x) 125 | y = int(y) 126 | row_size_list[x][y][1] = course_info_size[1] 127 | row_max = max(row_max, course_info_size[1]) 128 | 129 | # 单元格边距 130 | margin = 10 131 | 132 | # 确定每行的行高 133 | row_height_list = row_max + margin * 2 134 | 135 | # 确定每列的列宽 136 | column_width_list = 225 137 | 138 | # 确定表格底版的长和宽 139 | table_width = (column_num + 1) * column_width_list + 3 140 | table_height = (row_num + 1) * row_height_list + 3 141 | 142 | # 新建白板图片 143 | table = ImageFactory( 144 | Image.new('RGBA', (table_width, table_height), self.colors['white']) 145 | ) 146 | 147 | # 绘制基点和移动锚点 148 | initial_point, basis_point = (1, 1), [1, 1] 149 | 150 | # 为单元格添加box和绘制边框 151 | for row_id in range(row_num + 1): 152 | for col_id in range(column_num + 1): 153 | box_size = (column_width_list, row_height_list) 154 | table.add_box(f'box_{row_id}_{col_id}', 155 | tuple(basis_point), 156 | tuple(box_size)) 157 | table.rectangle(f'box_{row_id}_{col_id}', outline=self.colors['blue'], width=2) 158 | basis_point[0] += box_size[0] 159 | basis_point[0] = initial_point[0] 160 | basis_point[1] += row_height_list 161 | 162 | # 向单元格中填字 163 | first_lis = ['上课时间', '周一', '周二', '周三', '周四', '周五', '周六', '周日'] 164 | for i, text in enumerate(first_lis[:column_num + 1]): 165 | header = simple_text(text, self.basic_font_size, self.using_font, self.colors['blue']) 166 | table.img_paste( 167 | header, 168 | table.align_box(f'box_0_{i}', header, align='center'), 169 | isalpha=True 170 | ) 171 | 172 | # 每节课上下课时间,可根据需要自行更改 173 | exact_time = user_data['exact_time'] 174 | 175 | # 填时间 176 | for x in range(row_num): 177 | row_id = x + 1 178 | id_text = simple_text(f"{exact_time[str(row_id)]['start']} - {exact_time[str(row_id)]['end']}", 179 | self.basic_font_size, self.using_font, self.colors['blue']) 180 | table.img_paste( 181 | id_text, 182 | table.align_box(f'box_{row_id}_0', id_text, align='center'), 183 | isalpha=True 184 | ) 185 | for x in range(column_num + 1): 186 | if x == 0: 187 | continue 188 | for y in range(row_num + 1): 189 | if y == 0: 190 | continue 191 | info = "" 192 | x = str(x) 193 | y = str(y) 194 | try: 195 | for course in data[x][y]: 196 | # 判断是否是本周课表 197 | if current_week is not None and current_week not in course['week']: 198 | continue 199 | else: 200 | info += f"""{course["name"]}\n{course["teacher"]}\n{course["classroom"]}""" 201 | plugin_name_text = multi_text(info, 202 | box_size=(215, 0), 203 | default_font=self.using_font, 204 | default_color=self.colors['blue'], 205 | default_size=self.basic_font_size 206 | ) 207 | table.img_paste( 208 | plugin_name_text, 209 | table.align_box(f'box_{y}_{x}', plugin_name_text, align='center'), 210 | isalpha=True 211 | ) 212 | except KeyError: 213 | pass 214 | table_size = table.img.size 215 | 216 | # 添加注释 217 | note_basic_text = simple_text('注:没显示就是没有你的数据,请联系管理员', 218 | size=self.basic_font_size, 219 | color=self.colors['blue'], 220 | font=self.using_font) 221 | msg = "" 222 | if current_week is not None: 223 | msg = f"当前周数:{current_week}" 224 | note_text = multi_text(msg, 225 | box_size=(table_size[0] - 30 - note_basic_text.size[0] - 10, 0), 226 | default_font=self.using_font, 227 | default_color=self.colors['blue'], 228 | default_size=self.basic_font_size, 229 | spacing=4, 230 | horizontal_align="middle" 231 | ) 232 | note_img = ImageFactory( 233 | Image.new('RGBA', 234 | (note_text.size[0] + 10 + note_basic_text.size[0], 235 | max((note_text.size[1], note_basic_text.size[1]))), 236 | self.colors['white']) 237 | ) 238 | note_img.img_paste(note_basic_text, (0, 0), isalpha=True) 239 | note_img.img_paste(note_text, (note_basic_text.size[0] + 10, 0), isalpha=True) 240 | main_menu = ImageFactory( 241 | Image.new('RGBA', 242 | (table_size[0] + 140, table_size[1] + note_img.img.size[1] + 210), 243 | color=self.colors['white']) 244 | ) 245 | main_menu.img_paste( 246 | note_img.img, 247 | main_menu.align_box('self', table.img, pos=(0, 140), align='horizontal') 248 | ) 249 | main_menu.img_paste( 250 | table.img, 251 | main_menu.align_box('self', table.img, pos=(0, 160 + note_img.img.size[1]), align='horizontal') 252 | ) 253 | main_menu.add_box('border_box', 254 | main_menu.align_box('self', 255 | (table_size[0] + 40, table_size[1] + note_img.img.size[1] + 80), 256 | pos=(0, 100), 257 | align='horizontal'), 258 | (table_size[0] + 40, table_size[1] + note_img.img.size[1] + 90)) 259 | main_menu.rectangle('border_box', outline=self.colors['blue'], width=5) 260 | border_box_top_left = main_menu.boxes['border_box'].topLeft 261 | main_menu.add_box('title_box', (0, 0), (main_menu.get_size()[0], 100)) 262 | title = auto_resize_text(f"{user_id}的课表", 60, self.using_font, (table_width-60, 66), self.colors['blue']) 263 | main_menu.img_paste(title, main_menu.align_box('title_box', title, align='center'), isalpha=True) 264 | return main_menu.img 265 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import importlib 3 | from PIL import Image 4 | from pathlib import Path 5 | import datetime 6 | import json 7 | 8 | from nonebot.adapters.onebot.v11 import GroupMessageEvent 9 | from nonebot import logger, get_driver 10 | from pydantic import error_wrappers 11 | 12 | from .template import DefaultTemplate, PicTemplate 13 | 14 | """ 15 | 代码逻辑:存入数据(字典)→ 读取数据 → 填充表格 → 生成图片 16 | """ 17 | 18 | 19 | class DataManager(object): 20 | def __init__(self): 21 | """ 22 | 说明:初始化 23 | """ 24 | self.course_data = {} 25 | 26 | def load_class_info(self): 27 | """ 28 | 说明:加载课程信息 29 | :return: 30 | """ 31 | def load_from_json(_json_path: Path): 32 | """ 33 | 说明:加载json文件 34 | :param _json_path: 35 | :return: 36 | """ 37 | self.course_data = json.loads(_json_path.read_text(encoding='utf-8')) 38 | 39 | try: 40 | load_from_json(Path.cwd() / 'data' / 'course_config' / 'config.json') 41 | logger.success(f'课表数据加载成功') 42 | except json.JSONDecodeError as e: 43 | logger.opt(colors=True).error(f'课表 课表数据加载失败 (from json)\n' 44 | f'json解析失败: {e}') 45 | except error_wrappers.ValidationError as e: 46 | logger.opt(colors=True).error(f'课表 课表数据加载失败 (from json)\n' 47 | f'json缺少必要键值对: \n' 48 | f'{e}') 49 | return self.course_data 50 | 51 | 52 | class TemplateManager(object): 53 | def __init__(self): 54 | self.template_container = {'default': DefaultTemplate} # 模板装载对象 55 | self.templates_path = Path.cwd() / 'menu_config' / 'template' # 模板路径 56 | self.load_templates() 57 | 58 | def load_templates(self): # 从文件加载模板 59 | template_list = [template for template in self.templates_path.glob('*.py')] 60 | template_name_list = [template.stem for template in self.templates_path.glob('*.py')] 61 | for template_name, template_path in zip(template_name_list, template_list): 62 | template_spec = importlib.util.spec_from_file_location(template_name, template_path) 63 | template = importlib.util.module_from_spec(template_spec) 64 | template_spec.loader.exec_module(template) 65 | self.template_container.update({template_name: template.DefaultTemplate}) 66 | 67 | def select_template(self, template_name: str) -> PicTemplate: # 选择模板 68 | if template_name in self.template_container: 69 | return self.template_container[template_name] 70 | else: 71 | raise KeyError(f'There is no template named {template_name}') 72 | 73 | 74 | def get_weekday(): 75 | """ 76 | 获取今天是周几 77 | :return: int 78 | """ 79 | return datetime.datetime.now().weekday() + 1 80 | 81 | 82 | def get_rest_time(current, query_time): 83 | """ 84 | 将剩余时间的秒数转为天时分秒(若为0则不显示) 85 | """ 86 | diff = query_time - current 87 | d = diff // (24 * 3600) 88 | dr = diff % (24 * 3600) 89 | h = dr // 3600 90 | hr = dr % 3600 91 | m = hr // 60 92 | if d is not 0: 93 | msg = f"{d}天{h}小时{m}分" 94 | return msg 95 | elif h is not 0: 96 | msg = f"{h}小时{m}分" 97 | return msg 98 | else: 99 | msg = f"{m}分钟" 100 | return msg 101 | 102 | 103 | class CourseManager(object): 104 | """ 105 | 说明:课表管理类 106 | """ 107 | def __init__(self): 108 | self.cwd = Path.cwd() 109 | self.config_folder_make() 110 | self.data_manager = DataManager() 111 | self.template_manager = TemplateManager() 112 | # 上下课时间 113 | self.exact_time = { 114 | "1": {"start": "08:20", "end": "09:05"}, 115 | "2": {"start": "09:10", "end": "09:55"}, 116 | "3": {"start": "10:15", "end": "11:00"}, 117 | "4": {"start": "11:05", "end": "11:50"}, 118 | "5": {"start": "11:55", "end": "12:25"}, 119 | "6": {"start": "12:30", "end": "13:00"}, 120 | "7": {"start": "13:10", "end": "13:55"}, 121 | "8": {"start": "14:00", "end": "14:45"}, 122 | "9": {"start": "15:05", "end": "15:50"}, 123 | "10": {"start": "15:55", "end": "16:40"}, 124 | "11": {"start": "18:00", "end": "18:45"}, 125 | "12": {"start": "18:50", "end": "19:35"}, 126 | "13": {"start": "19:40", "end": "20:25"}, 127 | } 128 | 129 | def config_folder_make(self): 130 | """ 131 | 说明:创建配置文件夹 132 | :return: 133 | """ 134 | if not (self.cwd / 'data').exists(): 135 | (self.cwd / 'data').mkdir() 136 | if not (self.cwd / 'data' / 'course_config').exists(): 137 | (self.cwd / 'data' / 'course_config').mkdir() 138 | if not (self.cwd / 'data' / 'course_config' / 'fonts').exists(): 139 | (self.cwd / 'data' / 'course_config' / 'fonts').mkdir() 140 | if not (self.cwd / 'data' / 'course_config' / 'templates').exists(): 141 | (self.cwd / 'data' / 'course_config' / 'templates').mkdir() 142 | if not (self.cwd / 'data' / 'course_config' / 'menus').exists(): 143 | (self.cwd / 'data' / 'course_config' / 'menus').mkdir() 144 | # 创建默认字体 145 | if not (self.cwd / 'data' / 'course_config' / 'config.json').exists(): 146 | with (self.cwd / 'data' / 'course_config' / 'config.json').open('w', encoding='utf-8') as fp: 147 | fp.write(json.dumps({'default': 'simhei.ttf'})) 148 | 149 | def generate_timetable_image(self, event: GroupMessageEvent, week=None) -> Image: 150 | """ 151 | 说明:生成完整课表图片 152 | :param event: 153 | :param week: 154 | :return: 155 | """ 156 | self.init_user_data(event) 157 | if self.init_user_data(event) == -1: 158 | return f"暂时还没有你的数据哦,请联系超管创建一个你的课表吧" 159 | user_id = str(event.user_id) 160 | data = self.data_manager.course_data[user_id] 161 | template = self.template_manager.select_template('default') 162 | return template().generate_main_menu(data, event=event, current_week=week) 163 | 164 | def generate_week_image(self, event: GroupMessageEvent, week) -> Image: 165 | """ 166 | 生成当前周数的课表 167 | :param event: 168 | :param week: 169 | :return: 170 | """ 171 | self.init_user_data(event) 172 | if self.init_user_data(event) == -1: 173 | return f"暂时还没有你的数据哦,请联系超管创建一个你的课表吧" 174 | user_id = str(event.user_id) 175 | data = self.data_manager.course_data[user_id] 176 | template = self.template_manager.select_template('default') 177 | return template().generate_main_menu(data, event=event, current_week=week) 178 | 179 | def init_user_data(self, event: GroupMessageEvent): 180 | """ 181 | 说明:初始化用户数据 182 | :param event: 183 | :return: 184 | """ 185 | user_id = str(event.user_id) 186 | self.data_manager.course_data = self.data_manager.load_class_info() 187 | if user_id not in self.data_manager.course_data: 188 | self.blank_struct(event) 189 | return -1 190 | elif "exact_time" not in self.data_manager.course_data[user_id]: 191 | self.data_manager.course_data[user_id]["exact_time"] = self.exact_time 192 | self.save() 193 | self.data_manager.course_data[user_id] = self.data_manager.load_class_info()[user_id] 194 | else: 195 | self.data_manager.course_data[user_id] = self.data_manager.load_class_info()[user_id] 196 | 197 | def save(self): 198 | """ 199 | 保存数据 200 | :return: 201 | """ 202 | with (self.cwd / 'data' / 'course_config' / 'config.json').open('w', encoding='utf-8') as fp: 203 | json.dump(self.data_manager.course_data, fp, indent=4, ensure_ascii=False) 204 | 205 | def blank_struct(self, event: GroupMessageEvent): 206 | """ 207 | 新建user的空白课表结构存在json里 208 | :return: 209 | """ 210 | user_id = str(event.user_id) 211 | course_info = {"name": "", 212 | "teacher": "", 213 | "classroom": "", 214 | "week": []} 215 | 216 | # 新用户的周数默认初始化为1 217 | self.data_manager.course_data[user_id] = {'week': 1, 218 | 'exact_time': { 219 | "1": {"start": "08:20", "end": "09:05"}, 220 | "2": {"start": "09:10", "end": "09:55"}, 221 | "3": {"start": "10:15", "end": "11:00"}, 222 | "4": {"start": "11:05", "end": "11:50"}, 223 | "5": {"start": "11:55", "end": "12:25"}, 224 | "6": {"start": "12:30", "end": "13:00"}, 225 | "7": {"start": "13:10", "end": "13:55"}, 226 | "8": {"start": "14:00", "end": "14:45"}, 227 | "9": {"start": "15:05", "end": "15:50"}, 228 | "10": {"start": "15:55", "end": "16:40"}, 229 | "11": {"start": "18:00", "end": "18:45"}, 230 | "12": {"start": "18:50", "end": "19:35"}, 231 | "13": {"start": "19:40", "end": "20:25"}, 232 | }, 233 | } 234 | for i in range(7): 235 | i += 1 236 | self.data_manager.course_data[user_id][str(i)] = {} 237 | for j in range(13): 238 | j += 1 239 | self.data_manager.course_data[user_id][str(i)][str(j)] = [] 240 | self.data_manager.course_data[user_id][str(i)][str(j)].append(course_info) 241 | 242 | self.save() 243 | 244 | def auto_update_week(self): 245 | """ 246 | 更新周数 247 | :return: 248 | """ 249 | self.data_manager.course_data = self.data_manager.load_class_info() 250 | tmp_data = self.data_manager.course_data 251 | users_list = [] 252 | for user_id, value in tmp_data.items(): 253 | try: 254 | if user_id.isdigit() and value['week']: 255 | users_list.append(user_id) 256 | except KeyError: 257 | pass 258 | # py里面似乎没有for(auto)的语法,所以这里先添加了一次用户名单,再挨个更新周数 259 | for user in users_list: 260 | self.data_manager.course_data[user]['week'] += 1 261 | self.save() 262 | 263 | def set_week(self, event, week: int): 264 | """ 265 | 设置周数 266 | :param event: 267 | :param week: int 268 | :return: 269 | """ 270 | self.init_user_data(event) 271 | user_id = str(event.user_id) 272 | self.data_manager.course_data[user_id]['week'] = week 273 | self.save() 274 | 275 | def get_week(self, event): 276 | """ 277 | 获取周数 278 | :param event 279 | :return: int 280 | """ 281 | self.init_user_data(event) 282 | user_id = str(event.user_id) 283 | return self.data_manager.course_data[user_id]['week'] 284 | 285 | def now_course(self, event): 286 | """ 287 | 获取当前课程及最近的一节课(今日范围内) 288 | :param event: 289 | :return: 290 | """ 291 | # 获取当前周数 292 | current_week = self.get_week(event) 293 | 294 | # 获取当前是周几 295 | current_weekday = get_weekday() 296 | 297 | # 获取当前格式化时间 298 | now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 299 | msg = "" 300 | 301 | # 取前十位到日期 302 | current_day = now_time[:10] 303 | 304 | # 当前时间戳 305 | current_time_stamp = int(time.mktime(time.strptime(now_time, '%Y-%m-%d %H:%M:%S'))) 306 | today_data = self.data_manager.course_data[str(event.user_id)][str(current_weekday)] 307 | user_exact_time = self.data_manager.course_data[str(event.user_id)]["exact_time"] 308 | is_in_class = 0 309 | next_class = 0 310 | try: 311 | row_num = get_driver().config.row_num 312 | except: 313 | row_num = 13 314 | for i in range(1, row_num + 1): 315 | # 今日上下课时间 316 | course_start_time = f"{current_day} {user_exact_time[str(i)]['start']}" # 注意有空格 317 | course_end_time = f"{current_day} {user_exact_time[str(i)]['end']}" 318 | 319 | # 今日上下课时间的时间戳 320 | course_start_time_stamp = int(time.mktime(time.strptime(course_start_time, '%Y-%m-%d %H:%M'))) 321 | course_end_time_stamp = int(time.mktime(time.strptime(course_end_time, '%Y-%m-%d %H:%M'))) 322 | for course in today_data[str(i)]: 323 | if course_start_time_stamp <= current_time_stamp <= course_end_time_stamp\ 324 | and current_week in course['week']: 325 | msg += f"当前您正在上第{i}节课,为{course['name']},地点为{course['classroom']}\n还有{get_rest_time(current_time_stamp, course_end_time_stamp)}下课 " 326 | is_in_class = 1 327 | break 328 | for course in today_data[str(i)]: 329 | if current_time_stamp < course_start_time_stamp and current_week in course['week'] and next_class == 0: 330 | msg += f"今天的下一节课为{course['name']},地点为{course['classroom']}\n,上课时间为{user_exact_time[str(i)]['start']}\n还有{get_rest_time(current_time_stamp, course_start_time_stamp)}上课,请注意不要迟到 " 331 | next_class = 1 332 | break 333 | if is_in_class == 1 and next_class == 1: 334 | break 335 | 336 | if is_in_class == 0: 337 | msg = f"当前没有正在上的课\n" + msg 338 | 339 | if next_class == 0: 340 | msg = msg + f"今天剩下没有课了哦" 341 | weekday_info = "" 342 | if current_weekday == 1: 343 | weekday_info = "星期一" 344 | elif current_weekday == 2: 345 | weekday_info = "星期二" 346 | elif current_weekday == 3: 347 | weekday_info = "星期三" 348 | elif current_weekday == 4: 349 | weekday_info = "星期四" 350 | elif current_weekday == 5: 351 | weekday_info = "星期五" 352 | elif current_weekday == 6: 353 | weekday_info = "星期六" 354 | elif current_weekday == 7: 355 | weekday_info = "星期日" 356 | tmp = f"当前时间为{now_time},第{current_week}周{weekday_info}\n" 357 | msg = tmp + msg 358 | return msg 359 | 360 | def tomo_course(self, event): 361 | """ 362 | 获取明天第一节课的情报 363 | :param event: 364 | :return: 365 | """ 366 | # 获取当前周数 367 | current_week = self.get_week(event) 368 | 369 | # 获取当前是周几 370 | current_weekday = get_weekday() 371 | 372 | if current_weekday != 7: 373 | current_weekday += 1 374 | else: 375 | current_weekday = 1 376 | current_week += 1 377 | 378 | # 获取当前格式化时间 379 | now_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 380 | msg = "" 381 | 382 | # 获取明天的数据 383 | today_data = self.data_manager.course_data[str(event.user_id)][str(current_weekday)] 384 | user_exact_time = self.data_manager.course_data[str(event.user_id)]["exact_time"] 385 | next_class = 0 386 | is_8 = 0 387 | try: 388 | row_num = get_driver().config.row_num 389 | except: 390 | row_num = 13 391 | for i in range(1, row_num + 1): 392 | for course in today_data[str(i)]: 393 | if current_week in course['week']: 394 | msg += f"明天的第一节课为{course['name']},地点为{course['classroom']}\n,上课时间为{user_exact_time[str(i)]['start']}\n" 395 | next_class = 1 396 | if i == 1: 397 | is_8 = 1 398 | break 399 | if next_class == 1: 400 | break 401 | 402 | if next_class == 1: 403 | if is_8 == 1: 404 | msg = "明天有早八,记得早起捏\n" + msg 405 | else: 406 | msg = "明天没有早八,可以睡个懒觉捏\n" + msg 407 | else: 408 | msg = "明天没有课哦" 409 | 410 | weekday_info = "" 411 | if current_weekday == 1: 412 | weekday_info = "星期一" 413 | elif current_weekday == 2: 414 | weekday_info = "星期二" 415 | elif current_weekday == 3: 416 | weekday_info = "星期三" 417 | elif current_weekday == 4: 418 | weekday_info = "星期四" 419 | elif current_weekday == 5: 420 | weekday_info = "星期五" 421 | elif current_weekday == 6: 422 | weekday_info = "星期六" 423 | elif current_weekday == 7: 424 | weekday_info = "星期日" 425 | 426 | tmp = f"当前时间为{now_time},明天是第{current_week}周{weekday_info}\n" 427 | msg = tmp + msg 428 | return msg 429 | 430 | def get_exact_time(self, event): 431 | """ 432 | 获取用户上课时间 433 | :param event: 434 | :return: 435 | """ 436 | self.init_user_data(event) 437 | user_id = str(event.user_id) 438 | return self.data_manager.course_data[user_id]['exact_time'] 439 | 440 | 441 | # 实例化 442 | course_manager = CourseManager() 443 | --------------------------------------------------------------------------------