nonebot-plugin-course-schedule 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nonebot_plugin_course_schedule/__init__.py +56 -0
- nonebot_plugin_course_schedule/commands/bind_group.py +35 -0
- nonebot_plugin_course_schedule/commands/bind_schedule.py +175 -0
- nonebot_plugin_course_schedule/commands/group_schedule.py +125 -0
- nonebot_plugin_course_schedule/commands/show_today.py +117 -0
- nonebot_plugin_course_schedule/commands/weekly_ranking.py +84 -0
- nonebot_plugin_course_schedule/config.py +15 -0
- nonebot_plugin_course_schedule/resources/MapleMono-NF-CN-Medium.ttf +0 -0
- nonebot_plugin_course_schedule/utils/constants.py +48 -0
- nonebot_plugin_course_schedule/utils/data_manager.py +84 -0
- nonebot_plugin_course_schedule/utils/ics_parser.py +236 -0
- nonebot_plugin_course_schedule/utils/image_generator.py +519 -0
- nonebot_plugin_course_schedule-1.0.0.dist-info/METADATA +129 -0
- nonebot_plugin_course_schedule-1.0.0.dist-info/RECORD +16 -0
- nonebot_plugin_course_schedule-1.0.0.dist-info/WHEEL +4 -0
- nonebot_plugin_course_schedule-1.0.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from nonebot.plugin import PluginMetadata
|
|
2
|
+
from .config import Config, config
|
|
3
|
+
|
|
4
|
+
__plugin_meta__ = PluginMetadata(
|
|
5
|
+
name="电子课程表",
|
|
6
|
+
description="绑定课表、查看课程、查看群友的课程,以及……上课排行",
|
|
7
|
+
usage="""▶ 课表帮助:打印本信息
|
|
8
|
+
▶ 绑定课表:发送你的 .ics 文件或 WakeUp 分享口令来绑定课表
|
|
9
|
+
▷ 可以重新绑定,可以通过旦夕导出
|
|
10
|
+
▶ 解绑课表:删掉课表
|
|
11
|
+
▷ 解绑会将你解绑所有群聊
|
|
12
|
+
▶ 绑定群聊:让自己显示在本群的课表中
|
|
13
|
+
▷ 绑定课表时会自动绑定群聊
|
|
14
|
+
▶ 解绑群聊:让自己从本群的课表中消失
|
|
15
|
+
▶ 查看课表 <offset|date>:显示你今天要上的课程
|
|
16
|
+
▶ 群课表 <offset|date>:显示群友正在上的课和将要上的课
|
|
17
|
+
▶ 上课排行:看看苦逼群友本周上了多少课
|
|
18
|
+
""",
|
|
19
|
+
type="application",
|
|
20
|
+
homepage="https://github.com/GLDYM/nonebot-plugin-course-schedule",
|
|
21
|
+
config=Config,
|
|
22
|
+
supported_adapters={"~onebot.v11"},
|
|
23
|
+
extra={"author": "Polaris_Light", "version": "0.0.1", "priority": 5},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from nonebot import require
|
|
27
|
+
|
|
28
|
+
require("nonebot_plugin_apscheduler")
|
|
29
|
+
|
|
30
|
+
from typing import Union
|
|
31
|
+
from nonebot import on_command
|
|
32
|
+
from nonebot.adapters.onebot.v11 import (
|
|
33
|
+
GroupMessageEvent,
|
|
34
|
+
PrivateMessageEvent,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from .commands import (
|
|
38
|
+
bind_schedule,
|
|
39
|
+
bind_group,
|
|
40
|
+
show_today,
|
|
41
|
+
group_schedule,
|
|
42
|
+
weekly_ranking,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
help_cmd = on_command(
|
|
46
|
+
"course_help",
|
|
47
|
+
aliases={"课表帮助", "课程帮助"},
|
|
48
|
+
force_whitespace=True,
|
|
49
|
+
priority=5,
|
|
50
|
+
block=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@help_cmd.handle()
|
|
55
|
+
async def _(event: Union[GroupMessageEvent, PrivateMessageEvent]):
|
|
56
|
+
await help_cmd.finish(__plugin_meta__.usage)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from nonebot import on_command
|
|
4
|
+
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message
|
|
5
|
+
from ..utils.data_manager import data_manager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
bind_group = on_command(
|
|
9
|
+
"bind_group", aliases={"绑定群聊"}, force_whitespace=True, priority=5, block=True
|
|
10
|
+
)
|
|
11
|
+
unbind_group = on_command(
|
|
12
|
+
"unbind_group", aliases={"解绑群聊"}, force_whitespace=True, priority=5, block=True
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@bind_group.handle()
|
|
17
|
+
async def bind_group_handle(event: GroupMessageEvent):
|
|
18
|
+
group_id = event.group_id
|
|
19
|
+
user_id = event.user_id
|
|
20
|
+
|
|
21
|
+
ics_path = data_manager.get_ics_file_path(user_id)
|
|
22
|
+
if not os.path.exists(ics_path):
|
|
23
|
+
await bind_group.finish(f"请先绑定课表!")
|
|
24
|
+
|
|
25
|
+
data_manager.add_user_to_group(user_id, group_id)
|
|
26
|
+
await bind_group.finish(f"绑定群聊成功!")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@unbind_group.handle()
|
|
30
|
+
async def bind_group_handle(event: GroupMessageEvent):
|
|
31
|
+
group_id = event.group_id
|
|
32
|
+
user_id = event.user_id
|
|
33
|
+
|
|
34
|
+
data_manager.remove_user_from_group(user_id, group_id)
|
|
35
|
+
await bind_group.finish(f"解绑群聊成功!")
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
8
|
+
from nonebot import on_command, logger
|
|
9
|
+
from nonebot.matcher import Matcher
|
|
10
|
+
from nonebot.params import Arg
|
|
11
|
+
|
|
12
|
+
from nonebot.adapters.onebot.v11 import (
|
|
13
|
+
Bot,
|
|
14
|
+
GroupMessageEvent,
|
|
15
|
+
PrivateMessageEvent,
|
|
16
|
+
Message,
|
|
17
|
+
MessageSegment,
|
|
18
|
+
)
|
|
19
|
+
from nonebot_plugin_apscheduler import scheduler
|
|
20
|
+
|
|
21
|
+
from ..utils.data_manager import data_manager
|
|
22
|
+
from ..utils.ics_parser import ics_parser
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
bind_schedule = on_command(
|
|
26
|
+
"bind_schedule",
|
|
27
|
+
aliases={"绑定课表", "绑定课程"},
|
|
28
|
+
force_whitespace=True,
|
|
29
|
+
priority=5,
|
|
30
|
+
block=True,
|
|
31
|
+
)
|
|
32
|
+
unbind_schedule = on_command(
|
|
33
|
+
"unbind_schedule",
|
|
34
|
+
aliases={"解绑课表", "解绑课程"},
|
|
35
|
+
force_whitespace=True,
|
|
36
|
+
priority=5,
|
|
37
|
+
block=True,
|
|
38
|
+
)
|
|
39
|
+
binding_requests = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@bind_schedule.handle()
|
|
43
|
+
async def handle_bind_entry(
|
|
44
|
+
matcher: Matcher, event: Union[GroupMessageEvent, PrivateMessageEvent]
|
|
45
|
+
):
|
|
46
|
+
user_id = event.user_id
|
|
47
|
+
|
|
48
|
+
async def timeout():
|
|
49
|
+
await matcher.send(
|
|
50
|
+
MessageSegment.at(user_id)
|
|
51
|
+
+ "绑定请求已过期,请重新发送 绑定课表 命令以绑定。"
|
|
52
|
+
)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
scheduler.add_job(
|
|
56
|
+
func=timeout,
|
|
57
|
+
trigger="date",
|
|
58
|
+
run_date=datetime.now() + timedelta(seconds=60),
|
|
59
|
+
id=f"expire_bind_request_{user_id}",
|
|
60
|
+
replace_existing=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
await matcher.send("请在60秒内发送你的 .ics 文件或 WakeUp 分享口令。")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@bind_schedule.got("schedule_input", prompt="等待你的课表文件或口令中…")
|
|
67
|
+
async def handle_schedule_input(
|
|
68
|
+
bot: Bot,
|
|
69
|
+
matcher: Matcher,
|
|
70
|
+
event: Union[GroupMessageEvent, PrivateMessageEvent],
|
|
71
|
+
schedule_input: Message = Arg(),
|
|
72
|
+
):
|
|
73
|
+
group_id = event.group_id if isinstance(event, GroupMessageEvent) else None
|
|
74
|
+
user_id = event.user_id
|
|
75
|
+
scheduler.remove_job(f"expire_bind_request_{user_id}")
|
|
76
|
+
|
|
77
|
+
# await matcher.send(f"Arg: {str(schedule_input)}")
|
|
78
|
+
|
|
79
|
+
# Wake Up Token
|
|
80
|
+
token = ics_parser.parse_wakeup_token(str(schedule_input))
|
|
81
|
+
if token:
|
|
82
|
+
try:
|
|
83
|
+
json_data = await ics_parser.fetch_wakeup_schedule(token)
|
|
84
|
+
|
|
85
|
+
if not json_data:
|
|
86
|
+
await matcher.send(
|
|
87
|
+
"无法获取 WakeUp 课程表数据,请检查口令是否正确或已过期。"
|
|
88
|
+
)
|
|
89
|
+
return None
|
|
90
|
+
ics_content = ics_parser.convert_wakeup_to_ics(json_data)
|
|
91
|
+
if not ics_content:
|
|
92
|
+
await matcher.send("课程表数据解析失败,无法生成 ICS 文件。")
|
|
93
|
+
return None
|
|
94
|
+
ics_path = data_manager.get_ics_file_path(user_id)
|
|
95
|
+
with open(ics_path, "w", encoding="utf-8") as f:
|
|
96
|
+
f.write(ics_content)
|
|
97
|
+
|
|
98
|
+
if group_id:
|
|
99
|
+
data_manager.add_user_to_group(user_id, group_id)
|
|
100
|
+
|
|
101
|
+
ics_parser.clear_cache(ics_path)
|
|
102
|
+
await matcher.send("通过 WakeUp 口令绑定课表成功!")
|
|
103
|
+
return None
|
|
104
|
+
except Exception as e:
|
|
105
|
+
await matcher.finish(f"处理 WakeUp 口令失败: {e}")
|
|
106
|
+
logger.error(e)
|
|
107
|
+
|
|
108
|
+
# .ics 文件上传
|
|
109
|
+
for seg in schedule_input:
|
|
110
|
+
if seg.type == "file":
|
|
111
|
+
try:
|
|
112
|
+
file_id = seg.data.get("file_id")
|
|
113
|
+
file_url = await get_file_url(bot, event, file_id)
|
|
114
|
+
|
|
115
|
+
async with aiohttp.ClientSession() as session:
|
|
116
|
+
async with session.get(file_url) as resp:
|
|
117
|
+
ics_content = await resp.text()
|
|
118
|
+
|
|
119
|
+
ics_path = data_manager.get_ics_file_path(user_id)
|
|
120
|
+
with open(ics_path, "w", encoding="utf-8") as f:
|
|
121
|
+
f.write(ics_content)
|
|
122
|
+
|
|
123
|
+
# 防止炒饭
|
|
124
|
+
ics_parser.clear_cache(ics_path)
|
|
125
|
+
parsed = ics_parser.parse_ics_file(ics_path)
|
|
126
|
+
if parsed == []:
|
|
127
|
+
os.remove(ics_path)
|
|
128
|
+
raise ValueError("Not Valid ICS File.")
|
|
129
|
+
|
|
130
|
+
if group_id:
|
|
131
|
+
data_manager.add_user_to_group(user_id, group_id)
|
|
132
|
+
|
|
133
|
+
await matcher.send("课表文件绑定成功!")
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise e
|
|
137
|
+
# await matcher.finish(f"下载或保存课表文件失败:{e}")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
await matcher.finish(
|
|
141
|
+
"未识别的口令或文件格式,请确认是否为 WakeUp 分享口令或 .ics 文件。"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def get_file_url(
|
|
146
|
+
bot: Bot, event: Union[GroupMessageEvent, PrivateMessageEvent], file_id: str
|
|
147
|
+
) -> str:
|
|
148
|
+
if isinstance(event, GroupMessageEvent):
|
|
149
|
+
data = await bot.get_group_file_url(
|
|
150
|
+
group=event.group_id, group_id=event.group_id, file_id=file_id
|
|
151
|
+
)
|
|
152
|
+
return data["url"]
|
|
153
|
+
elif isinstance(event, PrivateMessageEvent):
|
|
154
|
+
data = await bot.get_private_file_url(user_id=bot.self_id, file_id=file_id)
|
|
155
|
+
return data["url"]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@unbind_schedule.handle()
|
|
159
|
+
async def handle_unbind_entry(event: Union[GroupMessageEvent, PrivateMessageEvent]):
|
|
160
|
+
user_id = event.user_id
|
|
161
|
+
|
|
162
|
+
ics_path = data_manager.get_ics_file_path(user_id)
|
|
163
|
+
if os.path.exists(ics_path):
|
|
164
|
+
os.remove(ics_path)
|
|
165
|
+
ics_parser.clear_cache(str(ics_path))
|
|
166
|
+
|
|
167
|
+
user_data = data_manager.load_user_data()
|
|
168
|
+
|
|
169
|
+
for group_id in list(user_data.keys()):
|
|
170
|
+
if user_id in user_data[group_id]:
|
|
171
|
+
user_data[group_id].remove(user_id)
|
|
172
|
+
|
|
173
|
+
data_manager.save_user_data(user_data)
|
|
174
|
+
|
|
175
|
+
await unbind_schedule.finish(f"解绑成功啦!")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shlex
|
|
3
|
+
from datetime import datetime, time, timezone, timedelta
|
|
4
|
+
from dateutil import parser
|
|
5
|
+
|
|
6
|
+
from nonebot import on_command, logger
|
|
7
|
+
from nonebot.adapters import Message
|
|
8
|
+
from nonebot.params import CommandArg
|
|
9
|
+
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageSegment
|
|
10
|
+
|
|
11
|
+
from ..utils.data_manager import data_manager
|
|
12
|
+
from ..utils.ics_parser import ics_parser
|
|
13
|
+
from ..utils.image_generator import image_generator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
group_schedule = on_command(
|
|
17
|
+
"group_schedule",
|
|
18
|
+
aliases={"群课表", "群友上什么", "群友在上什么", "群友在上什么课"},
|
|
19
|
+
priority=5,
|
|
20
|
+
force_whitespace=True,
|
|
21
|
+
block=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@group_schedule.handle()
|
|
26
|
+
async def _(bot: Bot, event: GroupMessageEvent, arg: Message = CommandArg()):
|
|
27
|
+
group_id = event.group_id
|
|
28
|
+
user_data = data_manager.load_user_data()
|
|
29
|
+
if str(group_id) not in user_data:
|
|
30
|
+
await group_schedule.send("本群还没有人绑定课表哦~")
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
user_ids = user_data[str(group_id)]
|
|
34
|
+
|
|
35
|
+
args = shlex.split(arg.extract_plain_text())
|
|
36
|
+
logger.info(f"{group_id} 查询群课表: {args}")
|
|
37
|
+
day = args[0].replace(".", "-") if args and args != [] else ""
|
|
38
|
+
|
|
39
|
+
shanghai_tz = timezone(timedelta(hours=8))
|
|
40
|
+
now = datetime.now(shanghai_tz)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
if day == "":
|
|
44
|
+
target_time = now
|
|
45
|
+
target_date = now.date()
|
|
46
|
+
elif day.isdigit():
|
|
47
|
+
offset_days = int(day)
|
|
48
|
+
target_date = now.date() + timedelta(days=offset_days)
|
|
49
|
+
target_time = datetime.combine(now.date(), time.min).astimezone(
|
|
50
|
+
shanghai_tz
|
|
51
|
+
) + timedelta(seconds=1)
|
|
52
|
+
else:
|
|
53
|
+
target_time = parser.parse(day)
|
|
54
|
+
target_date = target_time.date()
|
|
55
|
+
target_time = datetime.combine(now.date(), time.min).astimezone(
|
|
56
|
+
shanghai_tz
|
|
57
|
+
) + timedelta(seconds=1)
|
|
58
|
+
except Exception:
|
|
59
|
+
await group_schedule.finish(
|
|
60
|
+
"时间格式错误,请输入数字或日期,例如:3 或 2025-11-01"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
next_courses = []
|
|
64
|
+
|
|
65
|
+
for user_id in user_ids:
|
|
66
|
+
ics_path = data_manager.get_ics_file_path(user_id)
|
|
67
|
+
if not os.path.exists(ics_path):
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
courses = ics_parser.parse_ics_file(ics_path)
|
|
72
|
+
except Exception:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
today_courses = [c for c in courses if c["start_time"].date() == target_date]
|
|
76
|
+
current = next_ = None
|
|
77
|
+
|
|
78
|
+
for course in today_courses:
|
|
79
|
+
if course["start_time"] <= target_time < course["end_time"]:
|
|
80
|
+
current = course
|
|
81
|
+
break
|
|
82
|
+
elif course["start_time"] > target_time:
|
|
83
|
+
if not next_ or course["start_time"] < next_["start_time"]:
|
|
84
|
+
next_ = course
|
|
85
|
+
|
|
86
|
+
display = current or next_
|
|
87
|
+
user_info = await bot.get_group_member_info(group_id=group_id, user_id=user_id)
|
|
88
|
+
nickname = (
|
|
89
|
+
user_info["card"]
|
|
90
|
+
if user_info["card"] is not None and user_info["card"] != ""
|
|
91
|
+
else user_info["nickname"]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if display:
|
|
95
|
+
next_courses.append(
|
|
96
|
+
{
|
|
97
|
+
"summary": display["summary"],
|
|
98
|
+
"description": display["description"],
|
|
99
|
+
"location": display["location"],
|
|
100
|
+
"start_time": display["start_time"],
|
|
101
|
+
"end_time": display["end_time"],
|
|
102
|
+
"user_id": user_id,
|
|
103
|
+
"nickname": nickname,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
next_courses.append(
|
|
108
|
+
{
|
|
109
|
+
"summary": "今日无课",
|
|
110
|
+
"description": "",
|
|
111
|
+
"location": "",
|
|
112
|
+
"start_time": None,
|
|
113
|
+
"end_time": None,
|
|
114
|
+
"user_id": user_id,
|
|
115
|
+
"nickname": nickname,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not next_courses:
|
|
120
|
+
await group_schedule.send("群友们接下来都没有课啦!")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
next_courses.sort(key=lambda x: (x["start_time"] is None, x["start_time"]))
|
|
124
|
+
image_path = await image_generator.generate_schedule_image(next_courses)
|
|
125
|
+
await group_schedule.send(MessageSegment.image(image_path))
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shlex
|
|
3
|
+
from datetime import datetime, timezone, timedelta
|
|
4
|
+
from dateutil import parser
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
from nonebot import on_command, logger
|
|
8
|
+
from nonebot.adapters import Message
|
|
9
|
+
from nonebot.params import CommandArg
|
|
10
|
+
from nonebot.adapters.onebot.v11 import (
|
|
11
|
+
Bot,
|
|
12
|
+
GroupMessageEvent,
|
|
13
|
+
PrivateMessageEvent,
|
|
14
|
+
MessageSegment,
|
|
15
|
+
)
|
|
16
|
+
from ..utils.data_manager import data_manager
|
|
17
|
+
from ..utils.ics_parser import ics_parser
|
|
18
|
+
from ..utils.image_generator import image_generator
|
|
19
|
+
|
|
20
|
+
show_today = on_command(
|
|
21
|
+
"show_today",
|
|
22
|
+
aliases={"课表", "查看课表", "查看今日课表", "查看我的课表"},
|
|
23
|
+
force_whitespace=True,
|
|
24
|
+
priority=5,
|
|
25
|
+
block=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@show_today.handle()
|
|
30
|
+
async def _(
|
|
31
|
+
bot: Bot,
|
|
32
|
+
event: Union[GroupMessageEvent, PrivateMessageEvent],
|
|
33
|
+
arg: Message = CommandArg(),
|
|
34
|
+
):
|
|
35
|
+
group_id = event.group_id if isinstance(event, GroupMessageEvent) else None
|
|
36
|
+
user_id = event.user_id
|
|
37
|
+
|
|
38
|
+
args = shlex.split(arg.extract_plain_text())
|
|
39
|
+
logger.info(f"{user_id} 查询课表: {args}")
|
|
40
|
+
day = args[0].replace(".", "-") if args and args != [] else ""
|
|
41
|
+
|
|
42
|
+
shanghai_tz = timezone(timedelta(hours=8))
|
|
43
|
+
now = datetime.now(shanghai_tz)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
if day == "":
|
|
47
|
+
target_date = now.date()
|
|
48
|
+
mode = "today"
|
|
49
|
+
elif day.isdigit():
|
|
50
|
+
offset_days = int(day)
|
|
51
|
+
target_date = now.date() + timedelta(days=offset_days)
|
|
52
|
+
mode = "offset"
|
|
53
|
+
else:
|
|
54
|
+
target_time = parser.parse(day)
|
|
55
|
+
target_date = target_time.date()
|
|
56
|
+
mode = "specific"
|
|
57
|
+
except Exception:
|
|
58
|
+
await show_today.finish("时间格式错误,请输入数字或日期,例如:3 或 2025-11-01")
|
|
59
|
+
|
|
60
|
+
ics_path = data_manager.get_ics_file_path(user_id)
|
|
61
|
+
if not os.path.exists(ics_path):
|
|
62
|
+
await show_today.finish(
|
|
63
|
+
"你还没有绑定课表哦~请在群内发送 绑定课表 指令,然后发送 .ics 文件或 WakeUp 口令来绑定。"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
courses = ics_parser.parse_ics_file(ics_path)
|
|
67
|
+
if mode == "today":
|
|
68
|
+
# 不带参数,只显示剩下的课程
|
|
69
|
+
filtered_courses = [
|
|
70
|
+
c
|
|
71
|
+
for c in courses
|
|
72
|
+
if c["start_time"].date() == target_date and c["end_time"] > now
|
|
73
|
+
]
|
|
74
|
+
else:
|
|
75
|
+
# 指定天,查询当天全部课程(包括 0)
|
|
76
|
+
filtered_courses = [c for c in courses if c["start_time"].date() == target_date]
|
|
77
|
+
|
|
78
|
+
if not filtered_courses:
|
|
79
|
+
await show_today.finish("当日没有课啦!")
|
|
80
|
+
|
|
81
|
+
filtered_courses.sort(key=lambda x: x["start_time"])
|
|
82
|
+
|
|
83
|
+
# 那是谁?是谁?是谁? 那是复旦,复旦教务,复旦教务~
|
|
84
|
+
merged_courses = []
|
|
85
|
+
seen = {}
|
|
86
|
+
for course in filtered_courses:
|
|
87
|
+
key = (course["summary"], course["start_time"], course["end_time"])
|
|
88
|
+
if key in seen:
|
|
89
|
+
seen[key]["location"] += f", {course['location']}"
|
|
90
|
+
else:
|
|
91
|
+
seen[key] = course
|
|
92
|
+
merged_courses.append(course)
|
|
93
|
+
filtered_courses = merged_courses
|
|
94
|
+
|
|
95
|
+
if group_id:
|
|
96
|
+
user_info = await bot.get_group_member_info(group_id=group_id, user_id=user_id)
|
|
97
|
+
nickname = (
|
|
98
|
+
user_info["card"]
|
|
99
|
+
if user_info["card"] is not None and user_info["card"] != ""
|
|
100
|
+
else user_info["nickname"]
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
user_info = await bot.get_stranger_info(user_id=user_id)
|
|
104
|
+
nickname = user_info["nickname"]
|
|
105
|
+
|
|
106
|
+
for course in filtered_courses:
|
|
107
|
+
course["nickname"] = nickname
|
|
108
|
+
|
|
109
|
+
image_path = (
|
|
110
|
+
await image_generator.generate_user_schedule_image(filtered_courses, nickname)
|
|
111
|
+
if mode == "today"
|
|
112
|
+
else await image_generator.generate_user_schedule_image(
|
|
113
|
+
filtered_courses, nickname, target_date
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
await show_today.finish(MessageSegment.image(image_path))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from nonebot import on_command
|
|
2
|
+
from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageSegment
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from ..utils.data_manager import data_manager
|
|
5
|
+
from ..utils.ics_parser import ics_parser
|
|
6
|
+
from ..utils.image_generator import image_generator
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
weekly_ranking = on_command(
|
|
10
|
+
"weekly_ranking", aliases={"上课排行", "本周上课排行"}, priority=5, block=True
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@weekly_ranking.handle()
|
|
15
|
+
async def _(bot: Bot, event: GroupMessageEvent):
|
|
16
|
+
group_id = event.group_id
|
|
17
|
+
user_data = data_manager.load_user_data()
|
|
18
|
+
if str(group_id) not in user_data:
|
|
19
|
+
await weekly_ranking.send("本群还没有人绑定课表哦~")
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
user_ids = user_data[str(group_id)]
|
|
23
|
+
|
|
24
|
+
now = datetime.now(timezone(timedelta(hours=8)))
|
|
25
|
+
today = now.date()
|
|
26
|
+
start_of_week = today - timedelta(days=today.weekday())
|
|
27
|
+
end_of_week = start_of_week + timedelta(days=6)
|
|
28
|
+
|
|
29
|
+
ranking_data = []
|
|
30
|
+
|
|
31
|
+
for user_id in user_ids:
|
|
32
|
+
ics_path = data_manager.get_ics_file_path(user_id)
|
|
33
|
+
if not os.path.exists(ics_path):
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
courses = ics_parser.parse_ics_file(ics_path)
|
|
38
|
+
except Exception:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
total_duration = timedelta()
|
|
42
|
+
course_count = 0
|
|
43
|
+
seen = {}
|
|
44
|
+
|
|
45
|
+
for course in courses:
|
|
46
|
+
# 那是谁?是谁?是谁? 那是复旦,复旦教务,复旦教务~
|
|
47
|
+
key = (course["summary"], course["start_time"], course["end_time"])
|
|
48
|
+
if key in seen:
|
|
49
|
+
continue
|
|
50
|
+
else:
|
|
51
|
+
seen[key] = course
|
|
52
|
+
|
|
53
|
+
course_date = course["start_time"].date()
|
|
54
|
+
if start_of_week <= course_date <= end_of_week:
|
|
55
|
+
total_duration += course["end_time"] - course["start_time"]
|
|
56
|
+
course_count += 1
|
|
57
|
+
|
|
58
|
+
if course_count > 0:
|
|
59
|
+
user_info = await bot.get_group_member_info(
|
|
60
|
+
group_id=group_id, user_id=user_id
|
|
61
|
+
)
|
|
62
|
+
nickname = (
|
|
63
|
+
user_info["card"]
|
|
64
|
+
if user_info["card"] is not None and user_info["card"] != ""
|
|
65
|
+
else user_info["nickname"]
|
|
66
|
+
)
|
|
67
|
+
ranking_data.append(
|
|
68
|
+
{
|
|
69
|
+
"user_id": user_id,
|
|
70
|
+
"nickname": nickname,
|
|
71
|
+
"total_duration": total_duration,
|
|
72
|
+
"course_count": course_count,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not ranking_data:
|
|
77
|
+
await weekly_ranking.send("本周大家都没有课呢!")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
ranking_data.sort(key=lambda x: x["total_duration"], reverse=True)
|
|
81
|
+
image_path = await image_generator.generate_ranking_image(
|
|
82
|
+
ranking_data, start_of_week, end_of_week
|
|
83
|
+
)
|
|
84
|
+
await weekly_ranking.send(MessageSegment.image(image_path))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from nonebot import get_plugin_config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# fmt:off
|
|
8
|
+
class Config(BaseModel):
|
|
9
|
+
data_path: str = "data/course_schedule"
|
|
10
|
+
fort_path: str = Field(default_factory=lambda: str(
|
|
11
|
+
Path(__file__).parent / "resources" / "MapleMono-NF-CN-Medium.ttf"
|
|
12
|
+
))
|
|
13
|
+
# fmt:on
|
|
14
|
+
|
|
15
|
+
config = get_plugin_config(Config)
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
本模块用于存放课程表插件的常量
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# --- Group Schedule Image Styles ---
|
|
7
|
+
GS_BG_COLOR = "#FFFFFF"
|
|
8
|
+
GS_FONT_COLOR = "#333333"
|
|
9
|
+
GS_TITLE_COLOR = "#000000"
|
|
10
|
+
GS_SUBTITLE_COLOR = "#888888"
|
|
11
|
+
GS_STATUS_COLORS = {
|
|
12
|
+
"进行中": ("#D32F2F", "#FFFFFF"),
|
|
13
|
+
"下一节": ("#1976D2", "#FFFFFF"),
|
|
14
|
+
"已结束": ("#388E3C", "#FFFFFF"),
|
|
15
|
+
"无课程": ("#757575", "#FFFFFF"),
|
|
16
|
+
}
|
|
17
|
+
GS_AVATAR_SIZE = 80
|
|
18
|
+
GS_ROW_HEIGHT = 120
|
|
19
|
+
GS_PADDING = 40
|
|
20
|
+
GS_WIDTH = 1200
|
|
21
|
+
|
|
22
|
+
# --- User Schedule Image Styles ---
|
|
23
|
+
US_BG_COLOR = "#FFFFFF"
|
|
24
|
+
US_FONT_COLOR = "#333333"
|
|
25
|
+
US_TITLE_COLOR = "#000000"
|
|
26
|
+
US_SUBTITLE_COLOR = "#888888"
|
|
27
|
+
US_COURSE_BG_COLOR = "#E3F2FD"
|
|
28
|
+
US_ROW_HEIGHT = 100
|
|
29
|
+
US_ROW_PADDING = 15
|
|
30
|
+
US_ROW_SPACING = 5
|
|
31
|
+
US_ROW_MAX_UNIT = 66
|
|
32
|
+
US_PADDING = 40
|
|
33
|
+
US_WIDTH = 1000
|
|
34
|
+
US_SPACING = 5
|
|
35
|
+
US_MAX_UNIT = 38
|
|
36
|
+
|
|
37
|
+
# --- Ranking Image Styles ---
|
|
38
|
+
RANKING_BG_COLOR = "#FFFFFF"
|
|
39
|
+
RANKING_TITLE_COLOR = "#1F2937"
|
|
40
|
+
RANKING_SUBTITLE_COLOR = "#6B7280"
|
|
41
|
+
RANKING_FONT_COLOR = "#374151"
|
|
42
|
+
RANKING_ROW_BG_COLOR = "#F9FAFB"
|
|
43
|
+
RANKING_COLORS = {1: "#FBBF24", 2: "#9CA3AF", 3: "#F59E0B"}
|
|
44
|
+
RANKING_WIDTH = 1200
|
|
45
|
+
RANKING_PADDING = 60
|
|
46
|
+
RANKING_HEADER_HEIGHT = 160
|
|
47
|
+
RANKING_ROW_HEIGHT = 120
|
|
48
|
+
RANKING_AVATAR_SIZE = 80
|