@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 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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -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"}
@@ -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,5 @@
1
+ import type { EmailConfig } from '../types.js';
2
+ export declare class MailService {
3
+ send(config: EmailConfig): Promise<void>;
4
+ }
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }