@nbtca/nbtcal 0.1.0
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.
- package/README.md +235 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +115 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +75 -0
- package/dist/cli.js.map +1 -0
- package/dist/converter/index.d.ts +9 -0
- package/dist/converter/index.d.ts.map +1 -0
- package/dist/converter/index.js +49 -0
- package/dist/converter/index.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/mailer/index.d.ts +5 -0
- package/dist/mailer/index.d.ts.map +1 -0
- package/dist/mailer/index.js +46 -0
- package/dist/mailer/index.js.map +1 -0
- package/dist/scraper/index.d.ts +14 -0
- package/dist/scraper/index.d.ts.map +1 -0
- package/dist/scraper/index.js +179 -0
- package/dist/scraper/index.js.map +1 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# @nbtca/nbtcal
|
|
2
|
+
|
|
3
|
+
Extract course schedule from NBT campus educational system and export to ICS calendar format.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Authenticate with NBT WebVPN system
|
|
8
|
+
- Navigate educational system (jwxt.nbt.edu.cn)
|
|
9
|
+
- Extract course schedules (including electives and online courses)
|
|
10
|
+
- Convert to standard ICS calendar format
|
|
11
|
+
- Email delivery with calendar attachment
|
|
12
|
+
- Interactive CLI with user prompts
|
|
13
|
+
- Can be used as library or standalone tool
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @nbtca/nbtcal
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### As CLI Tool
|
|
24
|
+
|
|
25
|
+
First, configure SMTP settings via environment variables or GitHub Secrets:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export SMTP_HOST="smtp.example.com" # Optional, defaults to Gmail
|
|
29
|
+
export SMTP_PORT="587" # Optional, defaults to 587
|
|
30
|
+
export SMTP_SECURE="false" # Optional, defaults to false
|
|
31
|
+
export SMTP_USER="sender@example.com" # Required
|
|
32
|
+
export SMTP_PASS="your-smtp-password" # Required
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
nbtcal
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Follow the interactive prompts to:
|
|
42
|
+
1. Enter student ID and password
|
|
43
|
+
2. Provide semester start date
|
|
44
|
+
3. Enter recipient email address
|
|
45
|
+
|
|
46
|
+
### As Library
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { nbtcal } from '@nbtca/nbtcal';
|
|
50
|
+
|
|
51
|
+
// Option 1: Use environment variables for SMTP (recommended)
|
|
52
|
+
await nbtcal({
|
|
53
|
+
credentials: {
|
|
54
|
+
username: 'your-student-id',
|
|
55
|
+
password: 'your-password'
|
|
56
|
+
},
|
|
57
|
+
semesterStartDate: new Date('2024-09-02'),
|
|
58
|
+
email: 'recipient@example.com' // Recipient email only
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Option 2: Provide SMTP config programmatically
|
|
62
|
+
await nbtcal({
|
|
63
|
+
credentials: {
|
|
64
|
+
username: 'your-student-id',
|
|
65
|
+
password: 'your-password'
|
|
66
|
+
},
|
|
67
|
+
semesterStartDate: new Date('2024-09-02'),
|
|
68
|
+
email: 'recipient@example.com',
|
|
69
|
+
smtp: { // Optional - overrides environment variables
|
|
70
|
+
host: 'smtp.gmail.com',
|
|
71
|
+
port: 587,
|
|
72
|
+
secure: false,
|
|
73
|
+
user: 'sender@gmail.com',
|
|
74
|
+
pass: 'app-password'
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### SMTP Configuration
|
|
80
|
+
|
|
81
|
+
**For Administrators:** Configure the SMTP email service using environment variables or GitHub Secrets:
|
|
82
|
+
|
|
83
|
+
**Option 1: Environment Variables**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export SMTP_HOST="smtp.example.com" # SMTP server hostname
|
|
87
|
+
export SMTP_PORT="587" # SMTP port (587 for STARTTLS, 465 for SSL)
|
|
88
|
+
export SMTP_SECURE="false" # Set to "true" for SSL/TLS
|
|
89
|
+
export SMTP_USER="sender@example.com" # Sender email address
|
|
90
|
+
export SMTP_PASS="your-smtp-password" # Sender email password
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Option 2: .env File**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cp .env.example .env
|
|
97
|
+
# Edit .env with your SMTP settings
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Option 3: GitHub Secrets (Recommended for CI/CD)**
|
|
101
|
+
|
|
102
|
+
Configure in your repository:
|
|
103
|
+
- `SMTP_HOST` - SMTP server hostname
|
|
104
|
+
- `SMTP_PORT` - SMTP port (default: 587)
|
|
105
|
+
- `SMTP_SECURE` - Use SSL/TLS (default: false)
|
|
106
|
+
- `SMTP_USER` - Sender email address
|
|
107
|
+
- `SMTP_PASS` - Sender email password
|
|
108
|
+
|
|
109
|
+
**For Users:** Simply provide the recipient email address when prompted. No SMTP configuration needed.
|
|
110
|
+
|
|
111
|
+
**Supported Email Providers:**
|
|
112
|
+
- **Gmail** (smtp.gmail.com:587) - [Use app-specific password](https://support.google.com/accounts/answer/185833)
|
|
113
|
+
- **Outlook/Hotmail** (smtp-mail.outlook.com:587)
|
|
114
|
+
- **QQ Mail** (smtp.qq.com:587)
|
|
115
|
+
- **163 Mail** (smtp.163.com:465)
|
|
116
|
+
- **Custom SMTP** - Any standard SMTP server
|
|
117
|
+
|
|
118
|
+
**Testing Email Delivery:**
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# First, run the full flow test to generate schedule.ics
|
|
122
|
+
npx tsx test-full-flow.ts
|
|
123
|
+
|
|
124
|
+
# Then test email sending
|
|
125
|
+
npx tsx test-email.ts recipient@example.com
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Architecture
|
|
129
|
+
|
|
130
|
+
Following Unix philosophy: modular, composable, single-purpose components.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
src/
|
|
134
|
+
├── auth/ # WebVPN authentication
|
|
135
|
+
├── scraper/ # Schedule data extraction
|
|
136
|
+
├── converter/ # ICS format conversion
|
|
137
|
+
├── mailer/ # Email delivery
|
|
138
|
+
├── cli.ts # Interactive CLI
|
|
139
|
+
├── index.ts # Library exports
|
|
140
|
+
└── types.ts # TypeScript definitions
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Components
|
|
144
|
+
|
|
145
|
+
- **AuthService**: Handles WebVPN login and navigation
|
|
146
|
+
- **ScheduleScraper**: Extracts course data from web pages
|
|
147
|
+
- **ICSConverter**: Converts course data to ICS format
|
|
148
|
+
- **MailService**: Sends ICS file via email
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
### Setup
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm install
|
|
156
|
+
npm run build
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Testing
|
|
160
|
+
|
|
161
|
+
Run manual test with real credentials:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx tsx test-manual.ts
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This will walk through the entire workflow with pauses for verification.
|
|
168
|
+
|
|
169
|
+
### Project Status
|
|
170
|
+
|
|
171
|
+
✅ **Fully tested and working**
|
|
172
|
+
|
|
173
|
+
- ✅ Successfully authenticates via WebVPN
|
|
174
|
+
- ✅ Extracts course schedules using HTTP API (27 courses tested)
|
|
175
|
+
- ✅ Generates valid ICS calendar files (52KB output)
|
|
176
|
+
- ✅ Email delivery tested and working
|
|
177
|
+
- ✅ Tested with actual NBT credentials
|
|
178
|
+
|
|
179
|
+
See [DEVELOPMENT.md](./DEVELOPMENT.md) for detailed development guide.
|
|
180
|
+
See [TODO.md](./TODO.md) for roadmap and pending tasks.
|
|
181
|
+
|
|
182
|
+
## Integration with @nbtca/prompt
|
|
183
|
+
|
|
184
|
+
This package is designed to integrate with the `@nbtca/prompt` tool collection:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { nbtcal } from '@nbtca/nbtcal';
|
|
188
|
+
// Use in prompt tool workflows
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Security
|
|
192
|
+
|
|
193
|
+
- Credentials are never stored or logged
|
|
194
|
+
- Uses HTTP-only implementation (no browser automation)
|
|
195
|
+
- Password encrypted with AES-CBC before transmission
|
|
196
|
+
- SMTP passwords should use app-specific passwords
|
|
197
|
+
- Consider using environment variables for sensitive data
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
202
|
+
|
|
203
|
+
## Contributing
|
|
204
|
+
|
|
205
|
+
Contributions are welcome!
|
|
206
|
+
|
|
207
|
+
**Areas for improvement:**
|
|
208
|
+
1. Support for more SMTP providers
|
|
209
|
+
2. Better error handling and user feedback
|
|
210
|
+
3. Calendar app compatibility testing
|
|
211
|
+
4. Configuration file support
|
|
212
|
+
|
|
213
|
+
## Troubleshooting
|
|
214
|
+
|
|
215
|
+
### Login fails
|
|
216
|
+
- Verify credentials are correct
|
|
217
|
+
- Check if WebVPN is accessible
|
|
218
|
+
- Ensure network connectivity to NBT servers
|
|
219
|
+
|
|
220
|
+
### No courses extracted
|
|
221
|
+
- Verify semester selection is correct
|
|
222
|
+
- Check if you have courses in the selected semester
|
|
223
|
+
- Review API response in debug output
|
|
224
|
+
|
|
225
|
+
### Wrong dates in calendar
|
|
226
|
+
- Verify semester start date is correct
|
|
227
|
+
- Check week number parsing
|
|
228
|
+
- Confirm time slot mappings match NBT's schedule
|
|
229
|
+
|
|
230
|
+
### Email fails
|
|
231
|
+
- Verify SMTP credentials
|
|
232
|
+
- Use app-specific password for Gmail
|
|
233
|
+
- Check firewall/network restrictions
|
|
234
|
+
|
|
235
|
+
For more help, see [DEVELOPMENT.md](./DEVELOPMENT.md) debugging section.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AxiosInstance } from 'axios';
|
|
2
|
+
import type { Credentials } from '../types.js';
|
|
3
|
+
export declare class AuthService {
|
|
4
|
+
private credentials;
|
|
5
|
+
private client;
|
|
6
|
+
private cookieJar;
|
|
7
|
+
constructor(credentials: Credentials);
|
|
8
|
+
login(): Promise<void>;
|
|
9
|
+
navigateToSchedule(): Promise<void>;
|
|
10
|
+
getClient(): AxiosInstance;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAc,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAK7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB/C,qBAAa,WAAW;IAIV,OAAO,CAAC,WAAW;IAH/B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,SAAS,CAAY;gBAET,WAAW,EAAE,WAAW;IAetC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgEtB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBzC,SAAS,IAAI,aAAa;CAG3B"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { CookieJar } from 'tough-cookie';
|
|
3
|
+
import { wrapper } from 'axios-cookiejar-support';
|
|
4
|
+
import * as cheerio from 'cheerio';
|
|
5
|
+
import CryptoJS from 'crypto-js';
|
|
6
|
+
function randomString(length) {
|
|
7
|
+
const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
|
|
8
|
+
let result = '';
|
|
9
|
+
for (let i = 0; i < length; i++) {
|
|
10
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
function encryptPassword(password, key) {
|
|
15
|
+
const randomStr = randomString(64);
|
|
16
|
+
const data = randomStr + password;
|
|
17
|
+
const iv = randomString(16);
|
|
18
|
+
const encrypted = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(key), { iv: CryptoJS.enc.Utf8.parse(iv), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
|
|
19
|
+
return encrypted.toString();
|
|
20
|
+
}
|
|
21
|
+
export class AuthService {
|
|
22
|
+
credentials;
|
|
23
|
+
client;
|
|
24
|
+
cookieJar;
|
|
25
|
+
constructor(credentials) {
|
|
26
|
+
this.credentials = credentials;
|
|
27
|
+
this.cookieJar = new CookieJar();
|
|
28
|
+
this.client = wrapper(axios.create({
|
|
29
|
+
jar: this.cookieJar,
|
|
30
|
+
withCredentials: true,
|
|
31
|
+
maxRedirects: 10,
|
|
32
|
+
validateStatus: () => true,
|
|
33
|
+
headers: {
|
|
34
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
35
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
36
|
+
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
|
37
|
+
}
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
async login() {
|
|
41
|
+
if (!this.credentials.username || !this.credentials.password) {
|
|
42
|
+
throw new Error('Username and password are required');
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
// Get login page
|
|
46
|
+
const loginPageResponse = await this.client.get('https://webvpn.nbt.edu.cn/');
|
|
47
|
+
if (loginPageResponse.status !== 200) {
|
|
48
|
+
throw new Error(`Failed to access WebVPN login page (Status: ${loginPageResponse.status}). Please check your network connection.`);
|
|
49
|
+
}
|
|
50
|
+
const $ = cheerio.load(loginPageResponse.data);
|
|
51
|
+
const form = $('#pwdFromId');
|
|
52
|
+
// Extract form data
|
|
53
|
+
const execution = form.find('input[name="execution"]').val();
|
|
54
|
+
const pwdEncryptSalt = form.find('#pwdEncryptSalt').val();
|
|
55
|
+
if (!pwdEncryptSalt) {
|
|
56
|
+
throw new Error('Failed to extract encryption salt from login page. The page structure may have changed.');
|
|
57
|
+
}
|
|
58
|
+
// Encrypt password
|
|
59
|
+
const encryptedPassword = encryptPassword(this.credentials.password, pwdEncryptSalt);
|
|
60
|
+
// Submit login
|
|
61
|
+
const actualUrl = loginPageResponse.request?.res?.responseUrl || 'https://webvpn.nbt.edu.cn/';
|
|
62
|
+
const submitUrl = new URL('/authserver/login', actualUrl).href;
|
|
63
|
+
const loginResponse = await this.client.post(submitUrl, new URLSearchParams({
|
|
64
|
+
username: this.credentials.username,
|
|
65
|
+
password: encryptedPassword,
|
|
66
|
+
_eventId: 'submit',
|
|
67
|
+
cllt: 'userNameLogin',
|
|
68
|
+
dllt: 'generalLogin',
|
|
69
|
+
execution
|
|
70
|
+
}).toString(), {
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
73
|
+
'Referer': actualUrl
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
// Check if login was successful by looking for error messages
|
|
77
|
+
const loginHtml = cheerio.load(loginResponse.data);
|
|
78
|
+
const errorMsg = loginHtml('#errorMsg, .alert-danger').text().trim();
|
|
79
|
+
if (errorMsg) {
|
|
80
|
+
throw new Error(`Login failed: ${errorMsg}. Please check your credentials.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.message.includes('Login failed') || error.message.includes('Failed to')) {
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
88
|
+
throw new Error('Cannot connect to NBT WebVPN. Please check your network connection.');
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Authentication error: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async navigateToSchedule() {
|
|
94
|
+
try {
|
|
95
|
+
// Enter educational system
|
|
96
|
+
await this.client.get('https://jwxt-443.webvpn.nbt.edu.cn/sso/jziotlogin');
|
|
97
|
+
await this.client.get('https://jwxt-443.webvpn.nbt.edu.cn/jwglxt/xtgl/index_initMenu.html');
|
|
98
|
+
// Access schedule page to initialize session
|
|
99
|
+
const schedulePageResponse = await this.client.get('https://jwxt-443.webvpn.nbt.edu.cn/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default');
|
|
100
|
+
if (schedulePageResponse.status !== 200) {
|
|
101
|
+
throw new Error('Failed to access schedule page. You may not have permission.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.message.includes('Failed to')) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Failed to navigate to schedule system: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
getClient() {
|
|
112
|
+
return this.client;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { nbtcal } from './index.js';
|
|
4
|
+
async function main() {
|
|
5
|
+
console.log('\n@nbtca/nbtcal - Course Schedule Exporter');
|
|
6
|
+
console.log('========================================\n');
|
|
7
|
+
try {
|
|
8
|
+
// Prompt for credentials
|
|
9
|
+
const credentials = await inquirer.prompt([
|
|
10
|
+
{
|
|
11
|
+
type: 'input',
|
|
12
|
+
name: 'username',
|
|
13
|
+
message: 'Student ID:',
|
|
14
|
+
validate: (input) => input.length > 0 || 'Student ID is required'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: 'password',
|
|
18
|
+
name: 'password',
|
|
19
|
+
message: 'Password:',
|
|
20
|
+
mask: '*',
|
|
21
|
+
validate: (input) => input.length > 0 || 'Password is required'
|
|
22
|
+
}
|
|
23
|
+
]);
|
|
24
|
+
// Prompt for semester start date
|
|
25
|
+
const semesterInfo = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: 'input',
|
|
28
|
+
name: 'startDate',
|
|
29
|
+
message: 'Semester start date (YYYY-MM-DD):',
|
|
30
|
+
default: '2025-02-24', // Example: Spring 2025 semester
|
|
31
|
+
validate: (input) => {
|
|
32
|
+
const date = new Date(input);
|
|
33
|
+
return !isNaN(date.getTime()) || 'Invalid date format (use YYYY-MM-DD)';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]);
|
|
37
|
+
// Prompt for recipient email only
|
|
38
|
+
const emailInfo = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'input',
|
|
41
|
+
name: 'email',
|
|
42
|
+
message: 'Recipient email address (where to send the schedule):',
|
|
43
|
+
validate: (input) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input) || 'Invalid email'
|
|
44
|
+
}
|
|
45
|
+
]);
|
|
46
|
+
// Check if SMTP is configured
|
|
47
|
+
if (!process.env.SMTP_USER || !process.env.SMTP_PASS) {
|
|
48
|
+
console.error('\n❌ Error: SMTP email service not configured.');
|
|
49
|
+
console.error('\nPlease set the following environment variables:');
|
|
50
|
+
console.error(' SMTP_HOST="smtp.gmail.com" # SMTP server (optional, defaults to Gmail)');
|
|
51
|
+
console.error(' SMTP_PORT="587" # SMTP port (optional, defaults to 587)');
|
|
52
|
+
console.error(' SMTP_SECURE="false" # Use SSL/TLS (optional, defaults to false)');
|
|
53
|
+
console.error(' SMTP_USER="your-email@example.com" # Required');
|
|
54
|
+
console.error(' SMTP_PASS="your-app-password" # Required\n');
|
|
55
|
+
console.error('For Gmail, use an app-specific password: https://support.google.com/accounts/answer/185833');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
console.log('\n');
|
|
59
|
+
// Run the main process
|
|
60
|
+
await nbtcal({
|
|
61
|
+
credentials: {
|
|
62
|
+
username: credentials.username,
|
|
63
|
+
password: credentials.password
|
|
64
|
+
},
|
|
65
|
+
semesterStartDate: new Date(semesterInfo.startDate),
|
|
66
|
+
email: emailInfo.email
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error('\n✗ Error:', error.message);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
main();
|
|
75
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;IAC1D,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;IAE1D,IAAI,CAAC;QACH,yBAAyB;QACzB,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;YACxC;gBACE,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,UAAU;gBAChB,OAAO,EAAE,aAAa;gBACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,wBAAwB;aAClE;YACD;gBACE,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,UAAU;gBAChB,OAAO,EAAE,WAAW;gBACpB,IAAI,EAAE,GAAG;gBACT,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,sBAAsB;aAChE;SACF,CAAC,CAAC;QAEH,iCAAiC;QACjC,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;YACzC;gBACE,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,WAAW;gBACjB,OAAO,EAAE,mCAAmC;gBAC5C,OAAO,EAAE,YAAY,EAAG,gCAAgC;gBACxD,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;oBAClB,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;oBAC7B,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,sCAAsC,CAAC;gBAC1E,CAAC;aACF;SACF,CAAC,CAAC;QAEH,kCAAkC;QAClC,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;YACtC;gBACE,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,OAAO;gBACb,OAAO,EAAE,uDAAuD;gBAChE,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,4BAA4B,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,eAAe;aACjF;SACF,CAAC,CAAC;QAEH,8BAA8B;QAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;YACrD,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;YAC/D,OAAO,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;YACnE,OAAO,CAAC,KAAK,CAAC,mFAAmF,CAAC,CAAC;YACnG,OAAO,CAAC,KAAK,CAAC,+EAA+E,CAAC,CAAC;YAC/F,OAAO,CAAC,KAAK,CAAC,mFAAmF,CAAC,CAAC;YACnG,OAAO,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;YAClE,OAAO,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;YACpE,OAAO,CAAC,KAAK,CAAC,4FAA4F,CAAC,CAAC;YAC5G,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAElB,uBAAuB;QACvB,MAAM,MAAM,CAAC;YACX,WAAW,EAAE;gBACX,QAAQ,EAAE,WAAW,CAAC,QAAQ;gBAC9B,QAAQ,EAAE,WAAW,CAAC,QAAQ;aAC/B;YACD,iBAAiB,EAAE,IAAI,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC;YACnD,KAAK,EAAE,SAAS,CAAC,KAAK;SACvB,CAAC,CAAC;IAEL,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CourseSchedule } from '../types.js';
|
|
2
|
+
export declare class ICSConverter {
|
|
3
|
+
private semesterStartDate;
|
|
4
|
+
constructor(semesterStartDate: Date);
|
|
5
|
+
convert(courses: CourseSchedule[]): Buffer;
|
|
6
|
+
private createEvent;
|
|
7
|
+
private calculateDate;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/converter/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,qBAAa,YAAY;IACvB,OAAO,CAAC,iBAAiB,CAAO;gBAEpB,iBAAiB,EAAE,IAAI;IAInC,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,MAAM;IAmB1C,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,aAAa;CAYtB"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createEvents } from 'ics';
|
|
2
|
+
export class ICSConverter {
|
|
3
|
+
semesterStartDate;
|
|
4
|
+
constructor(semesterStartDate) {
|
|
5
|
+
this.semesterStartDate = semesterStartDate;
|
|
6
|
+
}
|
|
7
|
+
convert(courses) {
|
|
8
|
+
const events = [];
|
|
9
|
+
for (const course of courses) {
|
|
10
|
+
for (const week of course.weeks) {
|
|
11
|
+
const event = this.createEvent(course, week);
|
|
12
|
+
events.push(event);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const { error, value } = createEvents(events);
|
|
16
|
+
if (error) {
|
|
17
|
+
throw new Error(`Failed to create ICS: ${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
return Buffer.from(value, 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
createEvent(course, week) {
|
|
22
|
+
const date = this.calculateDate(week, course.weekday);
|
|
23
|
+
const [startHour, startMinute] = course.startTime.split(':').map(Number);
|
|
24
|
+
const [endHour, endMinute] = course.endTime.split(':').map(Number);
|
|
25
|
+
const description = course.courseType === 'practice'
|
|
26
|
+
? `实践教学\n教师: ${course.teacher}`
|
|
27
|
+
: `教师: ${course.teacher}`;
|
|
28
|
+
return {
|
|
29
|
+
title: course.name,
|
|
30
|
+
description,
|
|
31
|
+
location: course.location,
|
|
32
|
+
start: [date.getFullYear(), date.getMonth() + 1, date.getDate(), startHour, startMinute],
|
|
33
|
+
end: [date.getFullYear(), date.getMonth() + 1, date.getDate(), endHour, endMinute],
|
|
34
|
+
status: 'CONFIRMED',
|
|
35
|
+
busyStatus: 'BUSY'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
calculateDate(week, weekday) {
|
|
39
|
+
const date = new Date(this.semesterStartDate);
|
|
40
|
+
// Calculate days to add
|
|
41
|
+
// Week 1, Monday (weekday 1) = semesterStartDate
|
|
42
|
+
// Week 1, Tuesday (weekday 2) = semesterStartDate + 1
|
|
43
|
+
// Week 2, Monday (weekday 1) = semesterStartDate + 7
|
|
44
|
+
const daysToAdd = (week - 1) * 7 + (weekday - 1);
|
|
45
|
+
date.setDate(date.getDate() + daysToAdd);
|
|
46
|
+
return date;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/converter/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAwB,MAAM,KAAK,CAAC;AAGzD,MAAM,OAAO,YAAY;IACf,iBAAiB,CAAO;IAEhC,YAAY,iBAAuB;QACjC,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;IAC7C,CAAC;IAED,OAAO,CAAC,OAAyB;QAC/B,MAAM,MAAM,GAAsB,EAAE,CAAC;QAErC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBAChC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBAC7C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QAE9C,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CAAC,yBAAyB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5D,CAAC;QAED,OAAO,MAAM,CAAC,IAAI,CAAC,KAAM,EAAE,OAAO,CAAC,CAAC;IACtC,CAAC;IAEO,WAAW,CAAC,MAAsB,EAAE,IAAY;QACtD,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QACtD,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACzE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAEnE,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,KAAK,UAAU;YAClD,CAAC,CAAC,aAAa,MAAM,CAAC,OAAO,EAAE;YAC/B,CAAC,CAAC,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC;QAE5B,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,IAAI;YAClB,WAAW;YACX,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC;YACxF,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,SAAS,CAAC;YAClF,MAAM,EAAE,WAAW;YACnB,UAAU,EAAE,MAAM;SACnB,CAAC;IACJ,CAAC;IAEO,aAAa,CAAC,IAAY,EAAE,OAAe;QACjD,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAE9C,wBAAwB;QACxB,iDAAiD;QACjD,sDAAsD;QACtD,qDAAqD;QACrD,MAAM,SAAS,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QAEjD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC;IACd,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { AuthService } from './auth/index.js';
|
|
2
|
+
export { ScheduleScraper } from './scraper/index.js';
|
|
3
|
+
export { ICSConverter } from './converter/index.js';
|
|
4
|
+
export { MailService } from './mailer/index.js';
|
|
5
|
+
export type * from './types.js';
|
|
6
|
+
import type { Credentials, SemesterSelection, SMTPConfig } from './types.js';
|
|
7
|
+
export interface NbtcalOptions {
|
|
8
|
+
credentials: Credentials;
|
|
9
|
+
semester?: SemesterSelection;
|
|
10
|
+
semesterStartDate: Date;
|
|
11
|
+
email: string;
|
|
12
|
+
smtp?: SMTPConfig;
|
|
13
|
+
}
|
|
14
|
+
export declare function nbtcal(options: NbtcalOptions): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,mBAAmB,YAAY,CAAC;AAMhC,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7E,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,iBAAiB,EAAE,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAqDlE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export { AuthService } from './auth/index.js';
|
|
2
|
+
export { ScheduleScraper } from './scraper/index.js';
|
|
3
|
+
export { ICSConverter } from './converter/index.js';
|
|
4
|
+
export { MailService } from './mailer/index.js';
|
|
5
|
+
import { AuthService } from './auth/index.js';
|
|
6
|
+
import { ScheduleScraper } from './scraper/index.js';
|
|
7
|
+
import { ICSConverter } from './converter/index.js';
|
|
8
|
+
import { MailService } from './mailer/index.js';
|
|
9
|
+
export async function nbtcal(options) {
|
|
10
|
+
console.log('NBT Course Schedule Exporter');
|
|
11
|
+
console.log('============================\n');
|
|
12
|
+
// Step 1: Authentication
|
|
13
|
+
console.log('[1/5] Authenticating...');
|
|
14
|
+
const auth = new AuthService(options.credentials);
|
|
15
|
+
await auth.login();
|
|
16
|
+
console.log(' ✓ Login successful\n');
|
|
17
|
+
// Step 2: Navigate to schedule
|
|
18
|
+
console.log('[2/5] Accessing schedule system...');
|
|
19
|
+
await auth.navigateToSchedule();
|
|
20
|
+
console.log(' ✓ Ready\n');
|
|
21
|
+
// Step 3: Get semester and extract schedule
|
|
22
|
+
console.log('[3/5] Fetching schedule data...');
|
|
23
|
+
const scraper = new ScheduleScraper(auth.getClient());
|
|
24
|
+
let semester = options.semester;
|
|
25
|
+
if (!semester) {
|
|
26
|
+
semester = await scraper.getCurrentSemester();
|
|
27
|
+
console.log(` Auto-detected semester: ${semester.academicYear} - ${semester.semester}`);
|
|
28
|
+
}
|
|
29
|
+
const courses = await scraper.extractSchedule(semester);
|
|
30
|
+
console.log(` ✓ Found ${courses.length} courses\n`);
|
|
31
|
+
if (courses.length === 0) {
|
|
32
|
+
throw new Error('No courses found. Please check your semester selection.');
|
|
33
|
+
}
|
|
34
|
+
// Step 4: Convert to ICS
|
|
35
|
+
console.log('[4/5] Converting to ICS format...');
|
|
36
|
+
const converter = new ICSConverter(options.semesterStartDate);
|
|
37
|
+
const icsBuffer = converter.convert(courses);
|
|
38
|
+
console.log(` ✓ Generated ${icsBuffer.length} bytes\n`);
|
|
39
|
+
// Step 5: Send email
|
|
40
|
+
console.log('[5/5] Sending email...');
|
|
41
|
+
const mailer = new MailService();
|
|
42
|
+
await mailer.send({
|
|
43
|
+
to: options.email,
|
|
44
|
+
subject: 'Your Course Schedule',
|
|
45
|
+
body: `Your course schedule for semester ${semester.academicYear}-${semester.semester} is attached.\n\nTotal courses: ${courses.length}\n\nImport the attached ICS file into your calendar application.`,
|
|
46
|
+
attachment: icsBuffer,
|
|
47
|
+
filename: 'schedule.ics',
|
|
48
|
+
smtp: options.smtp
|
|
49
|
+
});
|
|
50
|
+
console.log(' ✓ Email sent\n');
|
|
51
|
+
console.log('============================');
|
|
52
|
+
console.log('Success! Check your email.');
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mailer/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,aAAa,CAAC;AAE3D,qBAAa,WAAW;IAChB,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CA6C/C"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
export class MailService {
|
|
3
|
+
async send(config) {
|
|
4
|
+
// Use provided SMTP config or fall back to environment variables with Gmail defaults
|
|
5
|
+
const smtpConfig = config.smtp || {
|
|
6
|
+
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
|
7
|
+
port: parseInt(process.env.SMTP_PORT || '587'),
|
|
8
|
+
secure: process.env.SMTP_SECURE === 'true',
|
|
9
|
+
user: process.env.SMTP_USER || '',
|
|
10
|
+
pass: process.env.SMTP_PASS || ''
|
|
11
|
+
};
|
|
12
|
+
if (!smtpConfig.user || !smtpConfig.pass) {
|
|
13
|
+
throw new Error('SMTP credentials not provided. Set SMTP_USER and SMTP_PASS environment variables or provide smtp config.');
|
|
14
|
+
}
|
|
15
|
+
// Create a transporter using SMTP
|
|
16
|
+
const transporter = nodemailer.createTransport({
|
|
17
|
+
host: smtpConfig.host,
|
|
18
|
+
port: smtpConfig.port,
|
|
19
|
+
secure: smtpConfig.secure,
|
|
20
|
+
auth: {
|
|
21
|
+
user: smtpConfig.user,
|
|
22
|
+
pass: smtpConfig.pass
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const mailOptions = {
|
|
26
|
+
from: smtpConfig.user,
|
|
27
|
+
to: config.to,
|
|
28
|
+
subject: config.subject,
|
|
29
|
+
text: config.body,
|
|
30
|
+
attachments: [
|
|
31
|
+
{
|
|
32
|
+
filename: config.filename,
|
|
33
|
+
content: config.attachment
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
await transporter.sendMail(mailOptions);
|
|
39
|
+
console.log(`Email sent successfully to ${config.to}`);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new Error(`Failed to send email: ${error}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mailer/index.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAC;AAGpC,MAAM,OAAO,WAAW;IACtB,KAAK,CAAC,IAAI,CAAC,MAAmB;QAC5B,qFAAqF;QACrF,MAAM,UAAU,GAAe,MAAM,CAAC,IAAI,IAAI;YAC5C,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,gBAAgB;YAC/C,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,KAAK,CAAC;YAC9C,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,KAAK,MAAM;YAC1C,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE;YACjC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE;SAClC,CAAC;QAEF,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CAAC,0GAA0G,CAAC,CAAC;QAC9H,CAAC;QAED,kCAAkC;QAClC,MAAM,WAAW,GAAG,UAAU,CAAC,eAAe,CAAC;YAC7C,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,IAAI,EAAE;gBACJ,IAAI,EAAE,UAAU,CAAC,IAAI;gBACrB,IAAI,EAAE,UAAU,CAAC,IAAI;aACtB;SACF,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG;YAClB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,WAAW,EAAE;gBACX;oBACE,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,OAAO,EAAE,MAAM,CAAC,UAAU;iBAC3B;aACF;SACF,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YACxC,OAAO,CAAC,GAAG,CAAC,8BAA8B,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,yBAAyB,KAAK,EAAE,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
import type { CourseSchedule, SemesterSelection } from '../types.js';
|
|
3
|
+
export declare class ScheduleScraper {
|
|
4
|
+
private client;
|
|
5
|
+
constructor(client: AxiosInstance);
|
|
6
|
+
extractSchedule(semester: SemesterSelection): Promise<CourseSchedule[]>;
|
|
7
|
+
private parseCourse;
|
|
8
|
+
private parsePracticeCourse;
|
|
9
|
+
private parseWeeks;
|
|
10
|
+
private parseTimeSlots;
|
|
11
|
+
private getTimeFromSlot;
|
|
12
|
+
getCurrentSemester(): Promise<SemesterSelection>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scraper/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAarE,qBAAa,eAAe;IACd,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAEnC,eAAe,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAkD7E,OAAO,CAAC,WAAW;IAwBnB,OAAO,CAAC,mBAAmB;IA6B3B,OAAO,CAAC,UAAU;IAwClB,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,eAAe;IAsBjB,kBAAkB,IAAI,OAAO,CAAC,iBAAiB,CAAC;CAgBvD"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
export class ScheduleScraper {
|
|
2
|
+
client;
|
|
3
|
+
constructor(client) {
|
|
4
|
+
this.client = client;
|
|
5
|
+
}
|
|
6
|
+
async extractSchedule(semester) {
|
|
7
|
+
const apiUrl = 'https://jwxt-443.webvpn.nbt.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html';
|
|
8
|
+
const schedulePageUrl = 'https://jwxt-443.webvpn.nbt.edu.cn/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default';
|
|
9
|
+
try {
|
|
10
|
+
const response = await this.client.post(apiUrl, new URLSearchParams({
|
|
11
|
+
xnm: semester.academicYear,
|
|
12
|
+
xqm: semester.semester
|
|
13
|
+
}).toString(), {
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
16
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
17
|
+
'Referer': schedulePageUrl
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
if (response.status !== 200) {
|
|
21
|
+
throw new Error(`Failed to fetch schedule data (Status: ${response.status}). The API may have changed or session expired.`);
|
|
22
|
+
}
|
|
23
|
+
if (!response.data) {
|
|
24
|
+
throw new Error('Empty response from schedule API');
|
|
25
|
+
}
|
|
26
|
+
const data = response.data;
|
|
27
|
+
const courses = [];
|
|
28
|
+
// Parse regular courses (kbList)
|
|
29
|
+
if (data.kbList && Array.isArray(data.kbList)) {
|
|
30
|
+
for (const item of data.kbList) {
|
|
31
|
+
courses.push(...this.parseCourse(item));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Parse practice courses (sjkList)
|
|
35
|
+
if (data.sjkList && Array.isArray(data.sjkList)) {
|
|
36
|
+
for (const item of data.sjkList) {
|
|
37
|
+
courses.push(...this.parsePracticeCourse(item));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return courses;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.message.includes('Failed to')) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Error fetching schedule: ${error.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
parseCourse(item) {
|
|
50
|
+
const name = item.kcmc || ''; // 课程名称
|
|
51
|
+
const teacher = item.xm || ''; // 教师
|
|
52
|
+
const location = item.cdmc || ''; // 教室
|
|
53
|
+
const weekday = parseInt(item.xqj) || 0; // 星期几
|
|
54
|
+
const weeks = this.parseWeeks(item.zcd || ''); // 周次
|
|
55
|
+
const timeSlots = this.parseTimeSlots(item.jcs || item.jc || ''); // 节次
|
|
56
|
+
if (!name || !weekday || weeks.length === 0 || !timeSlots) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return [{
|
|
60
|
+
name,
|
|
61
|
+
teacher,
|
|
62
|
+
location,
|
|
63
|
+
weekday,
|
|
64
|
+
startTime: timeSlots.startTime,
|
|
65
|
+
endTime: timeSlots.endTime,
|
|
66
|
+
weeks,
|
|
67
|
+
courseType: 'regular'
|
|
68
|
+
}];
|
|
69
|
+
}
|
|
70
|
+
parsePracticeCourse(item) {
|
|
71
|
+
const name = item.kcmc || '';
|
|
72
|
+
const teacher = item.jsxm || '';
|
|
73
|
+
const weeks = this.parseWeeks(item.qsjsz || '');
|
|
74
|
+
if (!name || weeks.length === 0) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
// Practice courses are usually all-day events
|
|
78
|
+
// We'll create events for Monday through Friday of each week
|
|
79
|
+
const courses = [];
|
|
80
|
+
for (let weekday = 1; weekday <= 5; weekday++) {
|
|
81
|
+
courses.push({
|
|
82
|
+
name: `${name} (实践)`,
|
|
83
|
+
teacher,
|
|
84
|
+
location: '无',
|
|
85
|
+
weekday,
|
|
86
|
+
startTime: '08:00',
|
|
87
|
+
endTime: '17:00',
|
|
88
|
+
weeks,
|
|
89
|
+
courseType: 'practice'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return courses;
|
|
93
|
+
}
|
|
94
|
+
parseWeeks(weekStr) {
|
|
95
|
+
// Examples: "1-16周", "9-16周", "1-8周", "14-16周", "16周"
|
|
96
|
+
const weeks = [];
|
|
97
|
+
if (!weekStr)
|
|
98
|
+
return weeks;
|
|
99
|
+
// Remove "周" character
|
|
100
|
+
weekStr = weekStr.replace(/周/g, '');
|
|
101
|
+
// Handle single week
|
|
102
|
+
if (!weekStr.includes('-') && !weekStr.includes(',')) {
|
|
103
|
+
const week = parseInt(weekStr);
|
|
104
|
+
if (!isNaN(week)) {
|
|
105
|
+
weeks.push(week);
|
|
106
|
+
}
|
|
107
|
+
return weeks;
|
|
108
|
+
}
|
|
109
|
+
// Handle ranges and lists
|
|
110
|
+
const parts = weekStr.split(',');
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
if (part.includes('-')) {
|
|
113
|
+
const [start, end] = part.split('-').map(s => parseInt(s.trim()));
|
|
114
|
+
if (!isNaN(start) && !isNaN(end)) {
|
|
115
|
+
for (let i = start; i <= end; i++) {
|
|
116
|
+
weeks.push(i);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const week = parseInt(part.trim());
|
|
122
|
+
if (!isNaN(week)) {
|
|
123
|
+
weeks.push(week);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return weeks;
|
|
128
|
+
}
|
|
129
|
+
parseTimeSlots(slotStr) {
|
|
130
|
+
// Examples: "3-4节", "3-4", "1-2"
|
|
131
|
+
if (!slotStr)
|
|
132
|
+
return null;
|
|
133
|
+
// Remove "节" character
|
|
134
|
+
slotStr = slotStr.replace(/节/g, '');
|
|
135
|
+
const match = slotStr.match(/(\d+)-(\d+)/);
|
|
136
|
+
if (!match)
|
|
137
|
+
return null;
|
|
138
|
+
const startSlot = parseInt(match[1]);
|
|
139
|
+
const endSlot = parseInt(match[2]);
|
|
140
|
+
return {
|
|
141
|
+
startTime: this.getTimeFromSlot(startSlot),
|
|
142
|
+
endTime: this.getTimeFromSlot(endSlot + 1)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
getTimeFromSlot(slot) {
|
|
146
|
+
// Standard class time slots for NBT
|
|
147
|
+
const slots = {
|
|
148
|
+
1: '08:00',
|
|
149
|
+
2: '08:50',
|
|
150
|
+
3: '09:50',
|
|
151
|
+
4: '10:40',
|
|
152
|
+
5: '11:30',
|
|
153
|
+
6: '13:30',
|
|
154
|
+
7: '14:20',
|
|
155
|
+
8: '15:20',
|
|
156
|
+
9: '16:10',
|
|
157
|
+
10: '17:00',
|
|
158
|
+
11: '18:30',
|
|
159
|
+
12: '19:20',
|
|
160
|
+
13: '20:10',
|
|
161
|
+
14: '21:00'
|
|
162
|
+
};
|
|
163
|
+
return slots[slot] || '08:00';
|
|
164
|
+
}
|
|
165
|
+
async getCurrentSemester() {
|
|
166
|
+
// Get current semester from schedule page
|
|
167
|
+
const schedulePageUrl = 'https://jwxt-443.webvpn.nbt.edu.cn/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default';
|
|
168
|
+
const response = await this.client.get(schedulePageUrl);
|
|
169
|
+
const cheerio = await import('cheerio');
|
|
170
|
+
const $ = cheerio.load(response.data);
|
|
171
|
+
const selectedYear = $('#xnm option[selected]').attr('value') || '2025';
|
|
172
|
+
const selectedSemester = $('#xqm option[selected]').attr('value') || '12';
|
|
173
|
+
return {
|
|
174
|
+
academicYear: selectedYear,
|
|
175
|
+
semester: selectedSemester
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scraper/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
username: string;
|
|
3
|
+
password: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SemesterSelection {
|
|
6
|
+
academicYear: string;
|
|
7
|
+
semester: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CourseSchedule {
|
|
10
|
+
name: string;
|
|
11
|
+
teacher: string;
|
|
12
|
+
location: string;
|
|
13
|
+
weekday: number;
|
|
14
|
+
startTime: string;
|
|
15
|
+
endTime: string;
|
|
16
|
+
weeks: number[];
|
|
17
|
+
courseType?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SMTPConfig {
|
|
20
|
+
host: string;
|
|
21
|
+
port: number;
|
|
22
|
+
secure: boolean;
|
|
23
|
+
user: string;
|
|
24
|
+
pass: string;
|
|
25
|
+
}
|
|
26
|
+
export interface EmailConfig {
|
|
27
|
+
to: string;
|
|
28
|
+
subject: string;
|
|
29
|
+
body: string;
|
|
30
|
+
attachment: Buffer;
|
|
31
|
+
filename: string;
|
|
32
|
+
smtp?: SMTPConfig;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nbtca/nbtcal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Extract course schedule from NBT campus system and export to ICS format",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nbtcal": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"start": "node dist/cli.js",
|
|
15
|
+
"test:flow": "npm run build && npx tsx test-full-flow.ts",
|
|
16
|
+
"test:email": "npm run build && npx tsx test-email.ts"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ics",
|
|
20
|
+
"calendar",
|
|
21
|
+
"schedule",
|
|
22
|
+
"nbt"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/nbtca/nbtcal.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/nbtca/nbtcal/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/nbtca/nbtcal#readme",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"axios": "^1.13.2",
|
|
41
|
+
"axios-cookiejar-support": "^6.0.5",
|
|
42
|
+
"cheerio": "^1.1.2",
|
|
43
|
+
"crypto-js": "^4.2.0",
|
|
44
|
+
"ics": "^3.8.1",
|
|
45
|
+
"inquirer": "^12.0.0",
|
|
46
|
+
"nodemailer": "^6.9.16",
|
|
47
|
+
"tough-cookie": "^5.1.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/crypto-js": "^4.2.2",
|
|
51
|
+
"@types/inquirer": "^9.0.7",
|
|
52
|
+
"@types/node": "^22.10.5",
|
|
53
|
+
"@types/nodemailer": "^6.4.17",
|
|
54
|
+
"typescript": "^5.7.2"
|
|
55
|
+
}
|
|
56
|
+
}
|