rhul-attendance-bot 0.1.48__tar.gz → 0.1.49__tar.gz
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.
- {rhul_attendance_bot-0.1.48/rhul_attendance_bot.egg-info → rhul_attendance_bot-0.1.49}/PKG-INFO +29 -14
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/README.md +28 -13
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/README_CN.md +28 -13
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/RHUL_attendance_bot.py +51 -3
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/app_paths.py +48 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/auto_login.py +24 -6
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/display_manager.py +1 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/fetch_ics.py +75 -2
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/pyproject.toml +1 -1
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49/rhul_attendance_bot.egg-info}/PKG-INFO +29 -14
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/LICENSE +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/MANIFEST.in +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/assets/discord_bot_screenshot.png +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/assets/ui_screenshot.png +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/discord_broadcast.py +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/local_2fa.py +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/rhul_attendance_bot.egg-info/SOURCES.txt +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/rhul_attendance_bot.egg-info/dependency_links.txt +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/rhul_attendance_bot.egg-info/entry_points.txt +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/rhul_attendance_bot.egg-info/requires.txt +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/rhul_attendance_bot.egg-info/top_level.txt +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/setup.cfg +0 -0
- {rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/update.py +0 -0
{rhul_attendance_bot-0.1.48/rhul_attendance_bot.egg-info → rhul_attendance_bot-0.1.49}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rhul-attendance-bot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.49
|
|
4
4
|
Summary: RHUL attendance automation bot
|
|
5
5
|
Author: PandaQuQ
|
|
6
6
|
License:
|
|
@@ -77,13 +77,26 @@ The RHUL Attendance Bot automates attendance marking for Royal Holloway students
|
|
|
77
77
|
|
|
78
78
|
Install Google Chrome if you don't have it yet: [download page](https://www.google.com/chrome/) (macOS users can also `brew install --cask google-chrome`).
|
|
79
79
|
|
|
80
|
+
Choose one of the two modes below:
|
|
81
|
+
|
|
82
|
+
1) **Recommended: Install via pip**
|
|
83
|
+
2) **Run from source**
|
|
84
|
+
|
|
85
|
+
### Mode 1: Install via pip (recommended)
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install rhul-attendance-bot
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Mode 2: Run from source
|
|
92
|
+
|
|
80
93
|
### Step 1: Clone the Repository
|
|
81
94
|
|
|
82
95
|
```bash
|
|
83
96
|
git clone https://github.com/PandaQuQ/RHUL_attendance_bot.git
|
|
84
97
|
```
|
|
85
98
|
|
|
86
|
-
### Step 2: Navigate to the Project Directory
|
|
99
|
+
### Step 2: Navigate to the Project Directory (source install only)
|
|
87
100
|
|
|
88
101
|
```bash
|
|
89
102
|
cd RHUL_attendance_bot
|
|
@@ -103,15 +116,7 @@ python3 -m venv venv
|
|
|
103
116
|
source venv/bin/activate
|
|
104
117
|
```
|
|
105
118
|
|
|
106
|
-
### Step 4: Install
|
|
107
|
-
|
|
108
|
-
#### Option A: Install from PyPI (recommended)
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
pip install rhul-attendance-bot
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
#### Option B: Install from source
|
|
119
|
+
### Step 4: Install dependencies
|
|
115
120
|
|
|
116
121
|
```bash
|
|
117
122
|
pip install -r requirements.txt
|
|
@@ -167,11 +172,20 @@ The bot now auto-downloads your timetable on first run and stores it in `ics/`.
|
|
|
167
172
|
```bash
|
|
168
173
|
rhul-attendance-bot -user Alice
|
|
169
174
|
```
|
|
170
|
-
|
|
175
|
+
- If `-user` is not provided and profiles exist, the app will list them and ask you to choose.
|
|
176
|
+
- If no profiles exist, it starts onboarding automatically and then renames the profile folder to your Profile Nickname.
|
|
177
|
+
|
|
178
|
+
5. **Cleanup (delete local data)**:
|
|
179
|
+
|
|
180
|
+
- Use `-clean` to remove all local app data under `~/.rhul_attendance_bot`:
|
|
181
|
+
```bash
|
|
182
|
+
rhul-attendance-bot -clean
|
|
183
|
+
```
|
|
171
184
|
|
|
172
|
-
|
|
185
|
+
6. **Keyboard Shortcuts**:
|
|
173
186
|
|
|
174
187
|
- **Manually Trigger the Next Event**: Press `[`, then `]`
|
|
188
|
+
- **Refresh Calendar (re-fetch ICS)**: Press `[`, then `c`
|
|
175
189
|
- **Exit the Script**: Press `[`, then `q`
|
|
176
190
|
|
|
177
191
|
## Important Notes
|
|
@@ -220,10 +234,11 @@ Current focus / future ideas:
|
|
|
220
234
|
- ✅ **Automatic Login**: Stored credentials + TOTP drive a fully automatic login flow.
|
|
221
235
|
- ✅ **2FA Code Reading**: OTP is generated locally from your saved secret.
|
|
222
236
|
- ✅ **Discord Webhook Bot**: Discord webhook notifications for attendance status, login, and lifecycle (optional; disable by leaving webhook URL empty)
|
|
237
|
+
- ✅ **PyPI Release**: Package published and installable via `pip install rhul-attendance-bot`
|
|
223
238
|
|
|
224
239
|
## License
|
|
225
240
|
|
|
226
|
-
This project is licensed under the MIT License
|
|
241
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
227
242
|
|
|
228
243
|
## Acknowledgments
|
|
229
244
|
|
|
@@ -32,13 +32,26 @@ The RHUL Attendance Bot automates attendance marking for Royal Holloway students
|
|
|
32
32
|
|
|
33
33
|
Install Google Chrome if you don't have it yet: [download page](https://www.google.com/chrome/) (macOS users can also `brew install --cask google-chrome`).
|
|
34
34
|
|
|
35
|
+
Choose one of the two modes below:
|
|
36
|
+
|
|
37
|
+
1) **Recommended: Install via pip**
|
|
38
|
+
2) **Run from source**
|
|
39
|
+
|
|
40
|
+
### Mode 1: Install via pip (recommended)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install rhul-attendance-bot
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Mode 2: Run from source
|
|
47
|
+
|
|
35
48
|
### Step 1: Clone the Repository
|
|
36
49
|
|
|
37
50
|
```bash
|
|
38
51
|
git clone https://github.com/PandaQuQ/RHUL_attendance_bot.git
|
|
39
52
|
```
|
|
40
53
|
|
|
41
|
-
### Step 2: Navigate to the Project Directory
|
|
54
|
+
### Step 2: Navigate to the Project Directory (source install only)
|
|
42
55
|
|
|
43
56
|
```bash
|
|
44
57
|
cd RHUL_attendance_bot
|
|
@@ -58,15 +71,7 @@ python3 -m venv venv
|
|
|
58
71
|
source venv/bin/activate
|
|
59
72
|
```
|
|
60
73
|
|
|
61
|
-
### Step 4: Install
|
|
62
|
-
|
|
63
|
-
#### Option A: Install from PyPI (recommended)
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
pip install rhul-attendance-bot
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
#### Option B: Install from source
|
|
74
|
+
### Step 4: Install dependencies
|
|
70
75
|
|
|
71
76
|
```bash
|
|
72
77
|
pip install -r requirements.txt
|
|
@@ -122,11 +127,20 @@ The bot now auto-downloads your timetable on first run and stores it in `ics/`.
|
|
|
122
127
|
```bash
|
|
123
128
|
rhul-attendance-bot -user Alice
|
|
124
129
|
```
|
|
125
|
-
|
|
130
|
+
- If `-user` is not provided and profiles exist, the app will list them and ask you to choose.
|
|
131
|
+
- If no profiles exist, it starts onboarding automatically and then renames the profile folder to your Profile Nickname.
|
|
132
|
+
|
|
133
|
+
5. **Cleanup (delete local data)**:
|
|
134
|
+
|
|
135
|
+
- Use `-clean` to remove all local app data under `~/.rhul_attendance_bot`:
|
|
136
|
+
```bash
|
|
137
|
+
rhul-attendance-bot -clean
|
|
138
|
+
```
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
6. **Keyboard Shortcuts**:
|
|
128
141
|
|
|
129
142
|
- **Manually Trigger the Next Event**: Press `[`, then `]`
|
|
143
|
+
- **Refresh Calendar (re-fetch ICS)**: Press `[`, then `c`
|
|
130
144
|
- **Exit the Script**: Press `[`, then `q`
|
|
131
145
|
|
|
132
146
|
## Important Notes
|
|
@@ -175,10 +189,11 @@ Current focus / future ideas:
|
|
|
175
189
|
- ✅ **Automatic Login**: Stored credentials + TOTP drive a fully automatic login flow.
|
|
176
190
|
- ✅ **2FA Code Reading**: OTP is generated locally from your saved secret.
|
|
177
191
|
- ✅ **Discord Webhook Bot**: Discord webhook notifications for attendance status, login, and lifecycle (optional; disable by leaving webhook URL empty)
|
|
192
|
+
- ✅ **PyPI Release**: Package published and installable via `pip install rhul-attendance-bot`
|
|
178
193
|
|
|
179
194
|
## License
|
|
180
195
|
|
|
181
|
-
This project is licensed under the MIT License
|
|
196
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
182
197
|
|
|
183
198
|
## Acknowledgments
|
|
184
199
|
|
|
@@ -32,13 +32,26 @@ RHUL 自动签到脚本通过使用网页自动化来为 Royal Holloway 学生
|
|
|
32
32
|
|
|
33
33
|
在安装前请先确保已安装 Google Chrome:可前往 [下载页面](https://www.google.com/chrome/)(macOS 也可使用 `brew install --cask google-chrome`)。
|
|
34
34
|
|
|
35
|
+
请从下面两种模式中选择:
|
|
36
|
+
|
|
37
|
+
1)**推荐:使用 pip 安装**
|
|
38
|
+
2)**源码运行**
|
|
39
|
+
|
|
40
|
+
### 模式 1:使用 pip 安装(推荐)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install rhul-attendance-bot
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 模式 2:源码运行
|
|
47
|
+
|
|
35
48
|
### 步骤 1:克隆代码仓库
|
|
36
49
|
|
|
37
50
|
```bash
|
|
38
51
|
git clone https://github.com/PandaQuQ/RHUL_attendance_bot.git
|
|
39
52
|
```
|
|
40
53
|
|
|
41
|
-
### 步骤 2
|
|
54
|
+
### 步骤 2:进入项目目录(仅源码安装需要)
|
|
42
55
|
|
|
43
56
|
```bash
|
|
44
57
|
cd RHUL_attendance_bot
|
|
@@ -58,15 +71,7 @@ python3 -m venv venv
|
|
|
58
71
|
source venv/bin/activate
|
|
59
72
|
```
|
|
60
73
|
|
|
61
|
-
### 步骤 4
|
|
62
|
-
|
|
63
|
-
#### 方式 A:通过 PyPI 安装(推荐)
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
pip install rhul-attendance-bot
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
#### 方式 B:从源码安装
|
|
74
|
+
### 步骤 4:安装依赖
|
|
70
75
|
|
|
71
76
|
```bash
|
|
72
77
|
pip install -r requirements.txt
|
|
@@ -122,11 +127,20 @@ pip install -r requirements.txt
|
|
|
122
127
|
```bash
|
|
123
128
|
rhul-attendance-bot -user 用户1
|
|
124
129
|
```
|
|
125
|
-
|
|
130
|
+
- 不传 `-user` 且已有 Profile 时,会列出并让你选择。
|
|
131
|
+
- 没有任何 Profile 时,会自动进入首次引导,完成后将 Profile 文件夹重命名为你的 Profile Nickname。
|
|
132
|
+
|
|
133
|
+
5. **清理本地数据**:
|
|
134
|
+
|
|
135
|
+
- 使用 `-clean` 删除 `~/.rhul_attendance_bot` 下的所有本地数据:
|
|
136
|
+
```bash
|
|
137
|
+
rhul-attendance-bot -clean
|
|
138
|
+
```
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
6. **快捷键说明**:
|
|
128
141
|
|
|
129
142
|
- **手动触发下一个事件**:按下 `[` 然后按 `]`
|
|
143
|
+
- **刷新课表(重新获取 ICS)**:按下 `[` 然后按 `c`
|
|
130
144
|
- **退出脚本**:按下 `[` 然后按 `q`
|
|
131
145
|
|
|
132
146
|
## 注意事项
|
|
@@ -175,10 +189,11 @@ Profile 数据存放在 `~/.rhul_attendance_bot/profiles/<profile_name>` 下,
|
|
|
175
189
|
- ✅ **读取 2FA 验证码**:使用本地秘钥生成 OTP 并自动填写。
|
|
176
190
|
- ✅ **自动登录**:已实现全自动登录流程。
|
|
177
191
|
- ✅ **Discord Hook Bot**:已支持 Discord Webhook 通知(可选,Webhook 置空即禁用),推送签到状态、登录和生命周期事件
|
|
192
|
+
- ✅ **PyPI 发布**:已发布,可通过 `pip install rhul-attendance-bot` 安装
|
|
178
193
|
|
|
179
194
|
## 许可证
|
|
180
195
|
|
|
181
|
-
本项目采用 MIT
|
|
196
|
+
本项目采用 MIT 许可证。有关详细信息,请参阅 [LICENSE](LICENSE) 文件。
|
|
182
197
|
|
|
183
198
|
## 鸣谢
|
|
184
199
|
|
|
@@ -19,6 +19,9 @@ from app_paths import (
|
|
|
19
19
|
get_ics_dir,
|
|
20
20
|
get_chrome_user_data_dir,
|
|
21
21
|
prompt_select_profile,
|
|
22
|
+
delete_app_data,
|
|
23
|
+
profile_exists,
|
|
24
|
+
list_profiles,
|
|
22
25
|
)
|
|
23
26
|
|
|
24
27
|
# Create a logger
|
|
@@ -85,9 +88,25 @@ def main():
|
|
|
85
88
|
import argparse
|
|
86
89
|
|
|
87
90
|
parser = argparse.ArgumentParser(description="RHUL attendance bot")
|
|
88
|
-
parser.add_argument("-user", "--user", dest="profile", default=None, help="
|
|
91
|
+
parser.add_argument("-user", "--user", dest="profile", default=None, help="Start with profile name")
|
|
92
|
+
parser.add_argument("-clean", action="store_true", help="Delete all local app data")
|
|
89
93
|
args = parser.parse_args()
|
|
90
|
-
|
|
94
|
+
if args.clean:
|
|
95
|
+
deleted = delete_app_data()
|
|
96
|
+
if deleted:
|
|
97
|
+
print("Local app data removed: ~/.rhul_attendance_bot")
|
|
98
|
+
else:
|
|
99
|
+
print("No local app data found to remove.")
|
|
100
|
+
return
|
|
101
|
+
profiles_before = list_profiles()
|
|
102
|
+
|
|
103
|
+
if args.profile:
|
|
104
|
+
if not profile_exists(args.profile):
|
|
105
|
+
print("Profile not exist.")
|
|
106
|
+
return
|
|
107
|
+
profile_name = args.profile
|
|
108
|
+
else:
|
|
109
|
+
profile_name = prompt_select_profile()
|
|
91
110
|
|
|
92
111
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
93
112
|
os.chdir(script_dir)
|
|
@@ -123,10 +142,22 @@ def main():
|
|
|
123
142
|
if first_run:
|
|
124
143
|
print('First run detected: running onboarding steps...')
|
|
125
144
|
# Run auto_login.py for first-time login
|
|
126
|
-
|
|
145
|
+
if profiles_before:
|
|
146
|
+
subprocess.run([sys.executable, os.path.join(script_dir, 'auto_login.py'), '--user', profile_name])
|
|
147
|
+
else:
|
|
148
|
+
subprocess.run([sys.executable, os.path.join(script_dir, 'auto_login.py')])
|
|
149
|
+
profiles_after = list_profiles()
|
|
150
|
+
if len(profiles_after) == 1:
|
|
151
|
+
profile_name = profiles_after[0]
|
|
127
152
|
# Run fetch_ics.py to get timetable
|
|
128
153
|
subprocess.run([sys.executable, os.path.join(script_dir, 'fetch_ics.py'), '--user', profile_name])
|
|
129
154
|
|
|
155
|
+
# Recompute profile paths if profile_name changed during onboarding
|
|
156
|
+
profile_dir = get_profile_dir(profile_name)
|
|
157
|
+
credentials_path = get_credentials_path(profile_name)
|
|
158
|
+
ics_folder = get_ics_dir(profile_name)
|
|
159
|
+
ics_file = os.path.join(ics_folder, 'student_timetable.ics')
|
|
160
|
+
|
|
130
161
|
# Now proceed to import the rest of the modules
|
|
131
162
|
import threading
|
|
132
163
|
import time
|
|
@@ -148,6 +179,7 @@ def main():
|
|
|
148
179
|
import ntplib # Used to check system time synchronization
|
|
149
180
|
from auto_login import attempt_login
|
|
150
181
|
from update import check_for_updates
|
|
182
|
+
from fetch_ics import fetch_ics_url, refresh_calendar, renew_calendar
|
|
151
183
|
from discord_broadcast import DiscordBroadcaster
|
|
152
184
|
from display_manager import DisplayManager
|
|
153
185
|
# Reuse helpers but add custom MFA fallback below
|
|
@@ -377,6 +409,7 @@ def main():
|
|
|
377
409
|
if exit_event.wait(timeout=min(sleep_duration, 60)):
|
|
378
410
|
break
|
|
379
411
|
|
|
412
|
+
|
|
380
413
|
def listen_for_keypress(upcoming_events, exit_event):
|
|
381
414
|
ctrl_pressed = [False] # Use a mutable object to share state
|
|
382
415
|
|
|
@@ -396,6 +429,21 @@ def main():
|
|
|
396
429
|
threading.Thread(target=automated_function, args=(next_event_time, next_event_name, upcoming_events), daemon=True).start()
|
|
397
430
|
else:
|
|
398
431
|
logger.warning("No upcoming events to process.")
|
|
432
|
+
elif key.char == 'c':
|
|
433
|
+
if ctrl_pressed[0]:
|
|
434
|
+
logger.info("Refreshing calendar (fetching ICS)...", extra={"gray": True})
|
|
435
|
+
threading.Thread(
|
|
436
|
+
target=renew_calendar,
|
|
437
|
+
args=(
|
|
438
|
+
upcoming_events,
|
|
439
|
+
events_lock,
|
|
440
|
+
profile_name,
|
|
441
|
+
load_calendar,
|
|
442
|
+
get_upcoming_events,
|
|
443
|
+
logger,
|
|
444
|
+
),
|
|
445
|
+
daemon=True,
|
|
446
|
+
).start()
|
|
399
447
|
elif key.char == 'q':
|
|
400
448
|
if ctrl_pressed[0]:
|
|
401
449
|
logger.info("Exit shortcut pressed. Terminating the script.")
|
|
@@ -26,6 +26,32 @@ def get_profile_dir(profile_name=None):
|
|
|
26
26
|
return profile_dir
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def profile_exists(profile_name):
|
|
30
|
+
base = os.path.join(get_app_dir(), PROFILES_DIR_NAME)
|
|
31
|
+
name = _normalize_profile(profile_name)
|
|
32
|
+
return os.path.isdir(os.path.join(base, name))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rename_profile(old_name, new_name):
|
|
36
|
+
old_norm = _normalize_profile(old_name)
|
|
37
|
+
new_norm = _normalize_profile(new_name)
|
|
38
|
+
if old_norm == new_norm:
|
|
39
|
+
return True
|
|
40
|
+
base = os.path.join(get_app_dir(), PROFILES_DIR_NAME)
|
|
41
|
+
old_path = os.path.join(base, old_norm)
|
|
42
|
+
new_path = os.path.join(base, new_norm)
|
|
43
|
+
if not os.path.isdir(old_path):
|
|
44
|
+
return False
|
|
45
|
+
if os.path.exists(new_path):
|
|
46
|
+
return False
|
|
47
|
+
os.makedirs(base, exist_ok=True)
|
|
48
|
+
try:
|
|
49
|
+
os.rename(old_path, new_path)
|
|
50
|
+
return True
|
|
51
|
+
except Exception:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
29
55
|
def list_profiles():
|
|
30
56
|
base = os.path.join(get_app_dir(), PROFILES_DIR_NAME)
|
|
31
57
|
if not os.path.isdir(base):
|
|
@@ -72,3 +98,25 @@ def get_chrome_user_data_dir(profile_name=None):
|
|
|
72
98
|
user_dir = os.path.join(get_profile_dir(profile_name), "chrome_user_data")
|
|
73
99
|
os.makedirs(user_dir, exist_ok=True)
|
|
74
100
|
return user_dir
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def delete_app_data():
|
|
104
|
+
app_dir = get_app_dir()
|
|
105
|
+
if not os.path.isdir(app_dir):
|
|
106
|
+
return False
|
|
107
|
+
for root, dirs, files in os.walk(app_dir, topdown=False):
|
|
108
|
+
for name in files:
|
|
109
|
+
try:
|
|
110
|
+
os.remove(os.path.join(root, name))
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
for name in dirs:
|
|
114
|
+
try:
|
|
115
|
+
os.rmdir(os.path.join(root, name))
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
try:
|
|
119
|
+
os.rmdir(app_dir)
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
return True
|
|
@@ -12,7 +12,13 @@ from selenium.webdriver.support import expected_conditions as EC
|
|
|
12
12
|
from selenium.webdriver.chrome.options import Options
|
|
13
13
|
from webdriver_manager.chrome import ChromeDriverManager
|
|
14
14
|
from local_2fa import bind, get_otp
|
|
15
|
-
from app_paths import
|
|
15
|
+
from app_paths import (
|
|
16
|
+
get_2fa_config_path,
|
|
17
|
+
get_credentials_path,
|
|
18
|
+
prompt_select_profile,
|
|
19
|
+
profile_exists,
|
|
20
|
+
rename_profile,
|
|
21
|
+
)
|
|
16
22
|
SECURITY_INFO_URL = 'https://mysignins.microsoft.com/security-info'
|
|
17
23
|
LOGIN_URL = 'https://mysignins.microsoft.com/security-info'
|
|
18
24
|
|
|
@@ -157,6 +163,11 @@ def first_time_setup(profile_name=None):
|
|
|
157
163
|
payload['discord_webhook_url'] = discord_webhook_url
|
|
158
164
|
payload['enable_discord_webhook'] = bool(discord_webhook_url) if enable_discord_webhook is None else bool(enable_discord_webhook)
|
|
159
165
|
|
|
166
|
+
# Align profile folder name to profile nickname if needed
|
|
167
|
+
if profile_name and profile_nickname and profile_name != profile_nickname:
|
|
168
|
+
rename_profile(profile_name, profile_nickname)
|
|
169
|
+
profile_name = profile_nickname
|
|
170
|
+
|
|
160
171
|
credentials_path = get_credentials_path(profile_name)
|
|
161
172
|
with open(credentials_path, 'w') as f:
|
|
162
173
|
json.dump(payload, f)
|
|
@@ -654,7 +665,7 @@ def renew_login(driver, expected_url=None, logger=None, profile_name=None):
|
|
|
654
665
|
current_url = driver.current_url
|
|
655
666
|
page_src = driver.page_source
|
|
656
667
|
if ('login.microsoftonline.com' in current_url) or ('mysignins.microsoft.com' in current_url) or ('loginfmt' in page_src):
|
|
657
|
-
_log(logger, logging.INFO, "Detected Microsoft login page; attempting auto login...")
|
|
668
|
+
_log(logger, logging.INFO, "Detected Microsoft login page; attempting auto login...", gray=True)
|
|
658
669
|
fill_ms_login(driver, username, password)
|
|
659
670
|
handle_mfa_code(driver)
|
|
660
671
|
handle_kmsi(driver)
|
|
@@ -665,7 +676,7 @@ def renew_login(driver, expected_url=None, logger=None, profile_name=None):
|
|
|
665
676
|
if expected_url:
|
|
666
677
|
try:
|
|
667
678
|
WebDriverWait(driver, 60).until(EC.url_contains(expected_url))
|
|
668
|
-
_log(logger, logging.INFO, "Login detected via URL match.")
|
|
679
|
+
_log(logger, logging.INFO, "Login detected via URL match.", gray=True)
|
|
669
680
|
return True
|
|
670
681
|
except Exception:
|
|
671
682
|
_log(logger, logging.ERROR if logger else logging.INFO, "Login not detected within timeout.")
|
|
@@ -686,7 +697,7 @@ def verify_login(driver, expected_url, max_wait_minutes=30, logger=None):
|
|
|
686
697
|
current_url = driver.current_url
|
|
687
698
|
|
|
688
699
|
if current_url == expected_url:
|
|
689
|
-
_log(logger, logging.INFO, "Already logged in.")
|
|
700
|
+
_log(logger, logging.INFO, "Already logged in.", gray=True)
|
|
690
701
|
return True
|
|
691
702
|
else:
|
|
692
703
|
_log(logger, logging.INFO, "Need to login.")
|
|
@@ -713,7 +724,7 @@ def attempt_login(driver, expected_url, broadcaster=None, logger=None, profile_n
|
|
|
713
724
|
_log(logger, logging.INFO, "Existing session is valid; skipping renew_login.", gray=True)
|
|
714
725
|
return True
|
|
715
726
|
except Exception:
|
|
716
|
-
_log(logger, logging.INFO, "Session check did not confirm login; attempting renew_login.")
|
|
727
|
+
_log(logger, logging.INFO, "Session check did not confirm login; attempting renew_login.", gray=True)
|
|
717
728
|
|
|
718
729
|
result = renew_login(driver, expected_url, logger=logger, profile_name=profile_name)
|
|
719
730
|
if result:
|
|
@@ -733,7 +744,14 @@ if __name__ == '__main__':
|
|
|
733
744
|
import argparse
|
|
734
745
|
|
|
735
746
|
parser = argparse.ArgumentParser(description="RHUL auto login setup")
|
|
747
|
+
parser.add_argument("-help", action="help", help="Show this help message and exit")
|
|
736
748
|
parser.add_argument("-user", "--user", dest="profile", default=None, help="Profile name")
|
|
737
749
|
args = parser.parse_args()
|
|
738
|
-
|
|
750
|
+
if args.profile:
|
|
751
|
+
if not profile_exists(args.profile):
|
|
752
|
+
print("Profile not exist.")
|
|
753
|
+
raise SystemExit(1)
|
|
754
|
+
profile_name = args.profile
|
|
755
|
+
else:
|
|
756
|
+
profile_name = prompt_select_profile()
|
|
739
757
|
first_time_setup(profile_name=profile_name)
|
|
@@ -229,6 +229,7 @@ class DisplayManager:
|
|
|
229
229
|
|
|
230
230
|
instructions = Text.from_markup(
|
|
231
231
|
"Press [yellow][[/yellow] then [yellow]][/yellow] to manually trigger the next event\n"
|
|
232
|
+
"Press [yellow][[/yellow] then [yellow]c[/yellow] to refresh the calendar\n"
|
|
232
233
|
"Press [yellow][[/yellow] then [yellow]q[/yellow] to exit the script",
|
|
233
234
|
justify="center",
|
|
234
235
|
)
|
|
@@ -8,7 +8,7 @@ from selenium.webdriver.support.ui import WebDriverWait, Select
|
|
|
8
8
|
from selenium.webdriver.support import expected_conditions as EC
|
|
9
9
|
from selenium.webdriver.chrome.options import Options
|
|
10
10
|
from webdriver_manager.chrome import ChromeDriverManager
|
|
11
|
-
from app_paths import get_credentials_path, get_ics_dir, prompt_select_profile
|
|
11
|
+
from app_paths import get_credentials_path, get_ics_dir, prompt_select_profile, profile_exists
|
|
12
12
|
TIMETABLE_URL = 'https://webtimetables.royalholloway.ac.uk/'
|
|
13
13
|
|
|
14
14
|
|
|
@@ -93,11 +93,84 @@ def fetch_ics_url(profile_name=None):
|
|
|
93
93
|
time.sleep(5)
|
|
94
94
|
driver.quit()
|
|
95
95
|
|
|
96
|
+
|
|
97
|
+
def refresh_calendar(profile_name=None, load_calendar_fn=None, get_upcoming_events_fn=None, logger=None):
|
|
98
|
+
def _log(msg, level="info"):
|
|
99
|
+
if logger:
|
|
100
|
+
try:
|
|
101
|
+
getattr(logger, level)(msg)
|
|
102
|
+
return
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
print(msg)
|
|
106
|
+
|
|
107
|
+
ics_path = os.path.join(get_ics_dir(profile_name), 'student_timetable.ics')
|
|
108
|
+
try:
|
|
109
|
+
if os.path.exists(ics_path):
|
|
110
|
+
os.remove(ics_path)
|
|
111
|
+
fetch_ics_url(profile_name=profile_name)
|
|
112
|
+
|
|
113
|
+
if load_calendar_fn and get_upcoming_events_fn:
|
|
114
|
+
calendar = load_calendar_fn(ics_path)
|
|
115
|
+
if not calendar:
|
|
116
|
+
_log("Failed to reload calendar after refresh.", level="error")
|
|
117
|
+
return None, ics_path
|
|
118
|
+
new_events = get_upcoming_events_fn(calendar)
|
|
119
|
+
return new_events, ics_path
|
|
120
|
+
return None, ics_path
|
|
121
|
+
except Exception as e:
|
|
122
|
+
_log(f"Failed to refresh calendar: {e}", level="error")
|
|
123
|
+
return None, ics_path
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def renew_calendar(upcoming_events, events_lock, profile_name=None, load_calendar_fn=None, get_upcoming_events_fn=None, logger=None):
|
|
127
|
+
new_events, _ = refresh_calendar(
|
|
128
|
+
profile_name=profile_name,
|
|
129
|
+
load_calendar_fn=load_calendar_fn,
|
|
130
|
+
get_upcoming_events_fn=get_upcoming_events_fn,
|
|
131
|
+
logger=logger,
|
|
132
|
+
)
|
|
133
|
+
if new_events is None:
|
|
134
|
+
return
|
|
135
|
+
with events_lock:
|
|
136
|
+
upcoming_events.clear()
|
|
137
|
+
upcoming_events.extend(new_events)
|
|
138
|
+
if new_events:
|
|
139
|
+
next_event_start, next_event_name, _, next_event_end = new_events[0]
|
|
140
|
+
local_next_event_start = next_event_start.astimezone()
|
|
141
|
+
duration = next_event_end - next_event_start
|
|
142
|
+
msg = (
|
|
143
|
+
f"Waiting for next event: [bold magenta]{next_event_name}[/bold magenta] at "
|
|
144
|
+
f"[bold cyan]{local_next_event_start.strftime('%Y-%m-%d %H:%M:%S')}[/bold cyan] "
|
|
145
|
+
f"(duration: [bold green]{str(duration).split('.')[0]}[/bold green])"
|
|
146
|
+
)
|
|
147
|
+
if logger:
|
|
148
|
+
logger.info(msg)
|
|
149
|
+
else:
|
|
150
|
+
print(msg)
|
|
151
|
+
else:
|
|
152
|
+
if logger:
|
|
153
|
+
logger.info("No upcoming events after refresh.")
|
|
154
|
+
else:
|
|
155
|
+
print("No upcoming events after refresh.")
|
|
156
|
+
|
|
157
|
+
if logger:
|
|
158
|
+
logger.info("Calendar refreshed successfully.", extra={"gray": True})
|
|
159
|
+
else:
|
|
160
|
+
print("Calendar refreshed successfully.")
|
|
161
|
+
|
|
96
162
|
if __name__ == '__main__':
|
|
97
163
|
import argparse
|
|
98
164
|
|
|
99
165
|
parser = argparse.ArgumentParser(description="RHUL timetable ICS fetcher")
|
|
166
|
+
parser.add_argument("-help", action="help", help="Show this help message and exit")
|
|
100
167
|
parser.add_argument("-user", "--user", dest="profile", default=None, help="Profile name")
|
|
101
168
|
args = parser.parse_args()
|
|
102
|
-
|
|
169
|
+
if args.profile:
|
|
170
|
+
if not profile_exists(args.profile):
|
|
171
|
+
print("Profile not exist.")
|
|
172
|
+
raise SystemExit(1)
|
|
173
|
+
profile_name = args.profile
|
|
174
|
+
else:
|
|
175
|
+
profile_name = prompt_select_profile()
|
|
103
176
|
fetch_ics_url(profile_name=profile_name)
|
{rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49/rhul_attendance_bot.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rhul-attendance-bot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.49
|
|
4
4
|
Summary: RHUL attendance automation bot
|
|
5
5
|
Author: PandaQuQ
|
|
6
6
|
License:
|
|
@@ -77,13 +77,26 @@ The RHUL Attendance Bot automates attendance marking for Royal Holloway students
|
|
|
77
77
|
|
|
78
78
|
Install Google Chrome if you don't have it yet: [download page](https://www.google.com/chrome/) (macOS users can also `brew install --cask google-chrome`).
|
|
79
79
|
|
|
80
|
+
Choose one of the two modes below:
|
|
81
|
+
|
|
82
|
+
1) **Recommended: Install via pip**
|
|
83
|
+
2) **Run from source**
|
|
84
|
+
|
|
85
|
+
### Mode 1: Install via pip (recommended)
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install rhul-attendance-bot
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Mode 2: Run from source
|
|
92
|
+
|
|
80
93
|
### Step 1: Clone the Repository
|
|
81
94
|
|
|
82
95
|
```bash
|
|
83
96
|
git clone https://github.com/PandaQuQ/RHUL_attendance_bot.git
|
|
84
97
|
```
|
|
85
98
|
|
|
86
|
-
### Step 2: Navigate to the Project Directory
|
|
99
|
+
### Step 2: Navigate to the Project Directory (source install only)
|
|
87
100
|
|
|
88
101
|
```bash
|
|
89
102
|
cd RHUL_attendance_bot
|
|
@@ -103,15 +116,7 @@ python3 -m venv venv
|
|
|
103
116
|
source venv/bin/activate
|
|
104
117
|
```
|
|
105
118
|
|
|
106
|
-
### Step 4: Install
|
|
107
|
-
|
|
108
|
-
#### Option A: Install from PyPI (recommended)
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
pip install rhul-attendance-bot
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
#### Option B: Install from source
|
|
119
|
+
### Step 4: Install dependencies
|
|
115
120
|
|
|
116
121
|
```bash
|
|
117
122
|
pip install -r requirements.txt
|
|
@@ -167,11 +172,20 @@ The bot now auto-downloads your timetable on first run and stores it in `ics/`.
|
|
|
167
172
|
```bash
|
|
168
173
|
rhul-attendance-bot -user Alice
|
|
169
174
|
```
|
|
170
|
-
|
|
175
|
+
- If `-user` is not provided and profiles exist, the app will list them and ask you to choose.
|
|
176
|
+
- If no profiles exist, it starts onboarding automatically and then renames the profile folder to your Profile Nickname.
|
|
177
|
+
|
|
178
|
+
5. **Cleanup (delete local data)**:
|
|
179
|
+
|
|
180
|
+
- Use `-clean` to remove all local app data under `~/.rhul_attendance_bot`:
|
|
181
|
+
```bash
|
|
182
|
+
rhul-attendance-bot -clean
|
|
183
|
+
```
|
|
171
184
|
|
|
172
|
-
|
|
185
|
+
6. **Keyboard Shortcuts**:
|
|
173
186
|
|
|
174
187
|
- **Manually Trigger the Next Event**: Press `[`, then `]`
|
|
188
|
+
- **Refresh Calendar (re-fetch ICS)**: Press `[`, then `c`
|
|
175
189
|
- **Exit the Script**: Press `[`, then `q`
|
|
176
190
|
|
|
177
191
|
## Important Notes
|
|
@@ -220,10 +234,11 @@ Current focus / future ideas:
|
|
|
220
234
|
- ✅ **Automatic Login**: Stored credentials + TOTP drive a fully automatic login flow.
|
|
221
235
|
- ✅ **2FA Code Reading**: OTP is generated locally from your saved secret.
|
|
222
236
|
- ✅ **Discord Webhook Bot**: Discord webhook notifications for attendance status, login, and lifecycle (optional; disable by leaving webhook URL empty)
|
|
237
|
+
- ✅ **PyPI Release**: Package published and installable via `pip install rhul-attendance-bot`
|
|
223
238
|
|
|
224
239
|
## License
|
|
225
240
|
|
|
226
|
-
This project is licensed under the MIT License
|
|
241
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
227
242
|
|
|
228
243
|
## Acknowledgments
|
|
229
244
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/rhul_attendance_bot.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/rhul_attendance_bot.egg-info/requires.txt
RENAMED
|
File without changes
|
{rhul_attendance_bot-0.1.48 → rhul_attendance_bot-0.1.49}/rhul_attendance_bot.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|