blackboard-upc 1.0.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/CHANGELOG.md +60 -0
- package/CLAUDE.md +70 -0
- package/README.md +222 -0
- package/package.json +48 -0
- package/run.js +8 -0
- package/src/api/assignments.ts +135 -0
- package/src/api/client.ts +41 -0
- package/src/api/courses.ts +110 -0
- package/src/auth/login.ts +139 -0
- package/src/auth/session.ts +44 -0
- package/src/commands/api-docs.ts +100 -0
- package/src/commands/assignments.ts +237 -0
- package/src/commands/courses.ts +284 -0
- package/src/commands/download.ts +149 -0
- package/src/commands/login.ts +68 -0
- package/src/index.ts +109 -0
- package/src/mcp/server.ts +222 -0
- package/src/types/index.ts +104 -0
- package/src/ui/theme.ts +34 -0
- package/tsconfig.json +15 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `blackboard-upc` will be documented here.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## [1.0.0] — 2026-03-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
#### Autenticación
|
|
12
|
+
- Login via **SAML SSO → Microsoft Azure AD** con Playwright (ventana del browser)
|
|
13
|
+
- Sesión persistida en `~/.blackboard-cli/session.json` (TTL 8h, permisos 600)
|
|
14
|
+
- Comandos `login`, `logout`, `whoami`, `status`
|
|
15
|
+
|
|
16
|
+
#### Cursos
|
|
17
|
+
- `courses list` — cursos inscritos con nombre, rol, estado y último acceso
|
|
18
|
+
- `courses get <id>` — detalle de un curso
|
|
19
|
+
- `courses contents <id>` — árbol de contenido navegable por carpetas
|
|
20
|
+
- `courses contents --type file|folder|assignment|document` — filtro por tipo
|
|
21
|
+
- `courses announcements <id>` — anuncios del curso
|
|
22
|
+
- `courses grades <id>` — notas del ciclo
|
|
23
|
+
|
|
24
|
+
#### Tareas
|
|
25
|
+
- `assignments list <id>` — tareas con fecha de entrega, nota actual y alertas de color
|
|
26
|
+
- `assignments list --pending` — solo las pendientes de entrega
|
|
27
|
+
- `assignments attempts <id> <columnId>` — historial de entregas
|
|
28
|
+
- `assignments submit` — entregar tarea con archivo (`-f`), texto (`-t`) o borrador (`--draft`)
|
|
29
|
+
|
|
30
|
+
#### Descargas
|
|
31
|
+
- `download <courseId> <contentId>` — descargar archivo adjunto individual
|
|
32
|
+
- `download-folder <courseId> <folderId>` — descarga recursiva de toda una carpeta
|
|
33
|
+
- `download-folder --filter <keyword>` — filtrar por nombre de archivo
|
|
34
|
+
|
|
35
|
+
#### API & Developer experience
|
|
36
|
+
- `api <METHOD> <path>` — passthrough a cualquier endpoint de la REST API
|
|
37
|
+
- `endpoints` — catálogo documentado de 22+ endpoints con parámetros
|
|
38
|
+
- Todos los comandos aceptan `--json` con spinners redirigidos a `stderr`
|
|
39
|
+
|
|
40
|
+
#### MCP Server
|
|
41
|
+
- Comando `mcp` — inicia un servidor MCP (stdio) para Claude Code y Claude Desktop
|
|
42
|
+
- 13 herramientas: `whoami`, `list_courses`, `get_course`, `list_contents`,
|
|
43
|
+
`list_announcements`, `list_assignments`, `list_attempts`, `get_grades`,
|
|
44
|
+
`list_attachments`, `download_attachment`, `submit_attempt`, `raw_api`, `system_version`
|
|
45
|
+
- `CLAUDE.md` — guía de comportamiento para agentes IA
|
|
46
|
+
|
|
47
|
+
#### UI
|
|
48
|
+
- Banner ASCII con color rojo UPC (`#E31837`)
|
|
49
|
+
- Prompt "¿Qué puedo hacer ahora?" tras login exitoso
|
|
50
|
+
- Paleta semántica: `ok` (verde), `fail` (rojo), `warn` (amarillo), `hint` (cyan)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Roadmap
|
|
55
|
+
|
|
56
|
+
- [ ] `npx` install sin clonar repo (publicación en npm)
|
|
57
|
+
- [ ] Refresh automático de sesión antes de expirar
|
|
58
|
+
- [ ] Notificaciones de entregas próximas (`assignments due`)
|
|
59
|
+
- [ ] Descarga de videos de grabaciones de clase
|
|
60
|
+
- [ ] Soporte para múltiples cuentas / ciclos simultáneos
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# blackboard-cli — Agent Guide
|
|
2
|
+
|
|
3
|
+
This CLI gives Claude direct access to UPC Aula Virtual (Blackboard Learn). Use it to help students check their courses, assignments, grades, and download materials — all without opening a browser.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Before using any tool, the user must be authenticated:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
blackboard login # opens browser for Microsoft SSO
|
|
11
|
+
blackboard whoami # verify session is active
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
If you get `Not authenticated`, ask the user to run `blackboard login`.
|
|
15
|
+
|
|
16
|
+
## Primary workflow
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
1. list_courses → find the relevant courseId
|
|
20
|
+
2. list_assignments <courseId> → see pending tasks + due dates
|
|
21
|
+
3. get_grades <courseId> → check current grades
|
|
22
|
+
4. list_contents <courseId> → browse course materials
|
|
23
|
+
5. list_contents <courseId> <parentId> → navigate into a subfolder
|
|
24
|
+
6. list_attachments <courseId> <contentId>→ find downloadable files
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Agent behavior rules
|
|
28
|
+
|
|
29
|
+
- **Always confirm before submitting** (`submit_attempt`). Show the user what will be submitted and ask for confirmation. Never submit silently.
|
|
30
|
+
- **Show grades in context** — when showing grades, also show the assignment name, max score, and due date if available.
|
|
31
|
+
- **Navigate content recursively** — if the user asks for materials, explore subfolders using `list_contents` with `parentId`.
|
|
32
|
+
- **Use `raw_api` for anything not covered** — the Blackboard REST API is extensive. If there's no specific tool, use `raw_api` with the correct endpoint.
|
|
33
|
+
- **Session errors are recoverable** — if you get a session error, tell the user to run `blackboard login` (not a fatal error).
|
|
34
|
+
- **Respect rate limits** — don't fan out more than 5 parallel API calls.
|
|
35
|
+
|
|
36
|
+
## Key IDs
|
|
37
|
+
|
|
38
|
+
Course IDs look like `_529580_1`. Content and column IDs follow the same pattern.
|
|
39
|
+
|
|
40
|
+
## Useful endpoints (via raw_api)
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
GET /learn/api/public/v1/users/me
|
|
44
|
+
GET /learn/api/public/v1/users/{userId}/courses
|
|
45
|
+
GET /learn/api/public/v1/courses/{courseId}/contents
|
|
46
|
+
GET /learn/api/public/v1/courses/{courseId}/contents/{id}/children
|
|
47
|
+
GET /learn/api/public/v1/courses/{courseId}/announcements
|
|
48
|
+
GET /learn/api/public/v2/courses/{courseId}/gradebook/columns
|
|
49
|
+
GET /learn/api/public/v2/courses/{courseId}/gradebook/columns/{id}/attempts
|
|
50
|
+
GET /learn/api/public/v1/courses/{courseId}/contents/{id}/attachments
|
|
51
|
+
GET /learn/api/public/v1/courses/{courseId}/contents/{id}/attachments/{id}/download
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## MCP tools available
|
|
55
|
+
|
|
56
|
+
| Tool | What it does |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `whoami` | Current student info |
|
|
59
|
+
| `system_version` | Server version |
|
|
60
|
+
| `list_courses` | All enrolled courses |
|
|
61
|
+
| `get_course` | Single course details |
|
|
62
|
+
| `list_contents` | Course materials tree |
|
|
63
|
+
| `list_announcements` | Course announcements |
|
|
64
|
+
| `list_assignments` | Tasks with due dates + grades |
|
|
65
|
+
| `list_attempts` | Submission history |
|
|
66
|
+
| `get_grades` | Full grade report for a course |
|
|
67
|
+
| `list_attachments` | Files in a content item |
|
|
68
|
+
| `download_attachment` | Download file (base64) |
|
|
69
|
+
| `submit_attempt` | Submit assignment (confirm first!) |
|
|
70
|
+
| `raw_api` | Any other Blackboard endpoint |
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# blackboard-cli
|
|
2
|
+
|
|
3
|
+
> Accede a **UPC Aula Virtual** desde la terminal — sin abrir el navegador.
|
|
4
|
+
|
|
5
|
+
CLI no oficial para estudiantes de la UPC. Consulta cursos, descarga materiales, revisa tareas y entregas directamente desde la línea de comandos. También expone un **servidor MCP** para que Claude lo use como herramientas nativas.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx blackboard-upc login
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Instalación
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Opción 1 — usar directamente con npx (sin instalar)
|
|
17
|
+
npx blackboard-upc login
|
|
18
|
+
|
|
19
|
+
# Opción 2 — instalar globalmente
|
|
20
|
+
npm install -g blackboard-upc
|
|
21
|
+
blackboard login
|
|
22
|
+
|
|
23
|
+
# Opción 3 — clonar el repo
|
|
24
|
+
git clone https://github.com/aleoroni/blackboard-cli
|
|
25
|
+
cd blackboard-cli
|
|
26
|
+
npm install
|
|
27
|
+
npx playwright install chromium
|
|
28
|
+
node run.js login
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Primeros pasos
|
|
34
|
+
|
|
35
|
+
### 1. Login
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
blackboard login
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Se abre una ventana del navegador con el login de Microsoft UPC. Inicia sesión con tu cuenta `u20XXXXXXX@upc.edu.pe` (incluye MFA si lo tienes). La ventana se cierra sola y la sesión queda guardada 8 horas.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
██████ ██ █████ ██████ ██ ██ ██████ ██████ █████ ██████ ██████
|
|
45
|
+
...
|
|
46
|
+
CLI no oficial para UPC Aula Virtual · Blackboard Learn
|
|
47
|
+
|
|
48
|
+
✓ Sesión guardada — expira en 8 horas
|
|
49
|
+
Usuario: Alejandro Daniel Oroncoy Almeyda
|
|
50
|
+
|
|
51
|
+
¿Qué puedo hacer ahora?
|
|
52
|
+
|
|
53
|
+
blackboard courses list ver tus cursos del ciclo
|
|
54
|
+
blackboard assignments list <id> ver tareas pendientes y notas
|
|
55
|
+
blackboard courses contents <id> explorar materiales
|
|
56
|
+
blackboard download-folder <id> <fid> descargar toda una carpeta
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Ver cursos y tareas
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
blackboard courses list
|
|
63
|
+
|
|
64
|
+
_529630_1 Fundamentos de Arquitectura de Software [Ultra]
|
|
65
|
+
_529580_1 Finanzas e Ingeniería Económica [Ultra]
|
|
66
|
+
_529533_1 Desarrollo de Soluciones IOT [Ultra]
|
|
67
|
+
_529760_1 Diseño de Experimentos de Ingeniería de Software [Ultra]
|
|
68
|
+
|
|
69
|
+
blackboard assignments list _529760_1
|
|
70
|
+
|
|
71
|
+
_13890556_1 Tarea 1 [manual]
|
|
72
|
+
Nota: sin entregar · Máx: 3 pts · Entrega: 05/03/2026 (vencida hace 23d)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Comandos
|
|
78
|
+
|
|
79
|
+
### Sesión
|
|
80
|
+
```bash
|
|
81
|
+
blackboard login # autenticación Microsoft SSO
|
|
82
|
+
blackboard logout # borrar sesión
|
|
83
|
+
blackboard whoami # usuario activo y tiempo restante
|
|
84
|
+
blackboard status # versión del servidor Blackboard
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Cursos
|
|
88
|
+
```bash
|
|
89
|
+
blackboard courses list
|
|
90
|
+
blackboard courses get <courseId>
|
|
91
|
+
blackboard courses contents <courseId>
|
|
92
|
+
blackboard courses contents <courseId> --parent <folderId> # navegar subcarpetas
|
|
93
|
+
blackboard courses contents <courseId> --type file|folder|assignment
|
|
94
|
+
blackboard courses announcements <courseId>
|
|
95
|
+
blackboard courses grades <courseId>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Tareas
|
|
99
|
+
```bash
|
|
100
|
+
blackboard assignments list <courseId> # tareas con nota y fecha
|
|
101
|
+
blackboard assignments list <courseId> --pending # solo pendientes
|
|
102
|
+
blackboard assignments attempts <courseId> <id> # historial de entregas
|
|
103
|
+
blackboard assignments submit <courseId> <id> -f tarea.pdf
|
|
104
|
+
blackboard assignments submit <courseId> <id> -t "Mi respuesta" -c "Comentario"
|
|
105
|
+
blackboard assignments submit <courseId> <id> -f borrador.pdf --draft
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Descargas
|
|
109
|
+
```bash
|
|
110
|
+
blackboard download <courseId> <contentId> # archivo individual
|
|
111
|
+
blackboard download-folder <courseId> <folderId> -o ./dir/ # carpeta completa
|
|
112
|
+
blackboard download-folder <courseId> <folderId> --filter "parcial"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### API raw / scripting
|
|
116
|
+
```bash
|
|
117
|
+
blackboard api GET /learn/api/public/v1/users/me
|
|
118
|
+
blackboard api GET /learn/api/public/v1/courses -q "limit=10"
|
|
119
|
+
blackboard endpoints # catálogo de todos los endpoints conocidos
|
|
120
|
+
blackboard endpoints --json # para pipelines
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Todos los comandos aceptan `--json`. Los spinners van a `stderr`, por lo que `--json 2>/dev/null` es JSON limpio.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Uso con Claude (MCP)
|
|
128
|
+
|
|
129
|
+
`blackboard-cli` incluye un servidor MCP que Claude puede usar como herramientas nativas.
|
|
130
|
+
|
|
131
|
+
### Claude Code
|
|
132
|
+
|
|
133
|
+
Añade a `.mcp.json` en tu proyecto:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"mcpServers": {
|
|
138
|
+
"blackboard": {
|
|
139
|
+
"command": "npx",
|
|
140
|
+
"args": ["blackboard-upc", "mcp"]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Claude Desktop
|
|
147
|
+
|
|
148
|
+
Edita `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"mcpServers": {
|
|
153
|
+
"blackboard": {
|
|
154
|
+
"command": "npx",
|
|
155
|
+
"args": ["blackboard-upc", "mcp"]
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> **Nota:** Si usas instalación global, reemplaza `npx blackboard-upc` por la ruta absoluta del binario (`which blackboard`).
|
|
162
|
+
|
|
163
|
+
### Herramientas MCP disponibles
|
|
164
|
+
|
|
165
|
+
| Herramienta | Descripción |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `whoami` | Info del estudiante autenticado |
|
|
168
|
+
| `list_courses` | Cursos inscritos |
|
|
169
|
+
| `get_course` | Detalle de un curso |
|
|
170
|
+
| `list_contents` | Árbol de materiales |
|
|
171
|
+
| `list_announcements` | Anuncios del curso |
|
|
172
|
+
| `list_assignments` | Tareas con fechas y notas |
|
|
173
|
+
| `list_attempts` | Historial de entregas |
|
|
174
|
+
| `get_grades` | Notas del ciclo |
|
|
175
|
+
| `list_attachments` | Archivos de un contenido |
|
|
176
|
+
| `download_attachment` | Descargar archivo (base64) |
|
|
177
|
+
| `submit_attempt` | Entregar tarea (pide confirmación) |
|
|
178
|
+
| `raw_api` | Cualquier endpoint de Blackboard |
|
|
179
|
+
|
|
180
|
+
Con Claude puedes hacer cosas como:
|
|
181
|
+
|
|
182
|
+
> *"¿Qué tareas tengo pendientes esta semana?"*
|
|
183
|
+
> *"Descárgame todos los exámenes del curso de Finanzas"*
|
|
184
|
+
> *"¿Cuál es mi nota actual en Arquitectura de Software?"*
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Cómo funciona la autenticación
|
|
189
|
+
|
|
190
|
+
UPC usa **SAML SSO → Microsoft Azure AD**. El CLI:
|
|
191
|
+
1. Abre Chromium (Playwright) en la URL SAML de UPC
|
|
192
|
+
2. Te muestra el login de Microsoft — tú ingresas tus credenciales
|
|
193
|
+
3. Captura las cookies de sesión automáticamente al redirigir a `/ultra`
|
|
194
|
+
4. Guarda todo en `~/.blackboard-cli/session.json` (permisos `600`)
|
|
195
|
+
|
|
196
|
+
La sesión dura **8 horas**. Después necesitas volver a hacer `login`.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Stack
|
|
201
|
+
|
|
202
|
+
- **TypeScript** + `tsx` — sin build step
|
|
203
|
+
- **Playwright** — maneja el flujo SAML/SSO
|
|
204
|
+
- **Axios** — llamadas a la REST API con cookies de sesión
|
|
205
|
+
- **Commander.js** — framework CLI
|
|
206
|
+
- **MCP SDK** — servidor para Claude
|
|
207
|
+
- **Chalk** + **Ora** — output en la terminal
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Notas
|
|
212
|
+
|
|
213
|
+
- Probado con Blackboard Learn `v4000.10.0` (UPC, 2026).
|
|
214
|
+
- CLI **no oficial** — sin afiliación con UPC ni Blackboard Inc.
|
|
215
|
+
- Úsalo solo con tu propia cuenta. Respeta los TOS de UPC.
|
|
216
|
+
- Las cookies se guardan localmente. No se envían a servidores externos.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Licencia
|
|
221
|
+
|
|
222
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blackboard-upc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI no oficial para UPC Aula Virtual (Blackboard Learn) — acceso desde la terminal y MCP para Claude",
|
|
5
|
+
"main": "run.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"blackboard": "./run.js",
|
|
8
|
+
"blackboard-upc": "./run.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"start": "node dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"blackboard",
|
|
17
|
+
"upc",
|
|
18
|
+
"aula-virtual",
|
|
19
|
+
"cli",
|
|
20
|
+
"mcp",
|
|
21
|
+
"claude",
|
|
22
|
+
"lms",
|
|
23
|
+
"peru"
|
|
24
|
+
],
|
|
25
|
+
"author": "Alejandro Oroncoy",
|
|
26
|
+
"homepage": "https://github.com/aleoroni/blackboard-cli",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/aleoroni/blackboard-cli.git"
|
|
30
|
+
},
|
|
31
|
+
"license": "ISC",
|
|
32
|
+
"type": "commonjs",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
35
|
+
"@types/inquirer": "^9.0.9",
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"axios": "^1.7.0",
|
|
38
|
+
"chalk": "^5.3.0",
|
|
39
|
+
"commander": "^12.1.0",
|
|
40
|
+
"form-data": "^4.0.5",
|
|
41
|
+
"inquirer": "^10.1.0",
|
|
42
|
+
"ora": "^8.1.0",
|
|
43
|
+
"playwright": "^1.47.0",
|
|
44
|
+
"tsx": "^4.19.0",
|
|
45
|
+
"typescript": "^5.5.0",
|
|
46
|
+
"zod": "^4.3.6"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/run.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run TypeScript directly without build step
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const tsx = path.join(__dirname, 'node_modules', '.bin', 'tsx');
|
|
6
|
+
const entry = path.join(__dirname, 'src', 'index.ts');
|
|
7
|
+
const result = spawnSync(tsx, [entry, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
8
|
+
process.exit(result.status ?? 0);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import FormData from 'form-data';
|
|
5
|
+
|
|
6
|
+
export interface GradeColumn {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
contentId?: string;
|
|
10
|
+
score?: { possible: number };
|
|
11
|
+
availability?: { available: string };
|
|
12
|
+
grading?: {
|
|
13
|
+
type: 'Attempts' | 'Manual' | 'Calculated';
|
|
14
|
+
due?: string;
|
|
15
|
+
attemptsAllowed?: number;
|
|
16
|
+
scoringModel?: string;
|
|
17
|
+
};
|
|
18
|
+
gradebookCategoryId?: string;
|
|
19
|
+
scoreProviderHandle?: string;
|
|
20
|
+
includeInCalculations?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Attempt {
|
|
24
|
+
id: string;
|
|
25
|
+
userId?: string;
|
|
26
|
+
status: string;
|
|
27
|
+
displayGrade?: { score?: number; text?: string };
|
|
28
|
+
score?: number;
|
|
29
|
+
text?: string;
|
|
30
|
+
studentComments?: string;
|
|
31
|
+
studentSubmission?: string;
|
|
32
|
+
created?: string;
|
|
33
|
+
modified?: string;
|
|
34
|
+
attemptDate?: string;
|
|
35
|
+
files?: Array<{ id: string; fileName: string; mimeType: string }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SubmitAttemptBody {
|
|
39
|
+
studentComments?: string;
|
|
40
|
+
studentSubmission?: string;
|
|
41
|
+
fileUploadIds?: string[];
|
|
42
|
+
status?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function listAssignments(
|
|
46
|
+
client: AxiosInstance,
|
|
47
|
+
courseId: string
|
|
48
|
+
): Promise<GradeColumn[]> {
|
|
49
|
+
const r = await client.get(`/learn/api/public/v2/courses/${courseId}/gradebook/columns`, {
|
|
50
|
+
params: { limit: 100 },
|
|
51
|
+
});
|
|
52
|
+
// Only return Attempts and Manual columns (student-relevant)
|
|
53
|
+
return (r.data.results as GradeColumn[]).filter(
|
|
54
|
+
(c) => c.grading?.type === 'Attempts' || c.grading?.type === 'Manual'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function getAssignment(
|
|
59
|
+
client: AxiosInstance,
|
|
60
|
+
courseId: string,
|
|
61
|
+
columnId: string
|
|
62
|
+
): Promise<GradeColumn> {
|
|
63
|
+
const r = await client.get(`/learn/api/public/v2/courses/${courseId}/gradebook/columns/${columnId}`);
|
|
64
|
+
return r.data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function listAttempts(
|
|
68
|
+
client: AxiosInstance,
|
|
69
|
+
courseId: string,
|
|
70
|
+
columnId: string
|
|
71
|
+
): Promise<Attempt[]> {
|
|
72
|
+
const r = await client.get(
|
|
73
|
+
`/learn/api/public/v2/courses/${courseId}/gradebook/columns/${columnId}/attempts`,
|
|
74
|
+
{ params: { limit: 20 } }
|
|
75
|
+
);
|
|
76
|
+
return r.data.results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function getAttempt(
|
|
80
|
+
client: AxiosInstance,
|
|
81
|
+
courseId: string,
|
|
82
|
+
columnId: string,
|
|
83
|
+
attemptId: string
|
|
84
|
+
): Promise<Attempt> {
|
|
85
|
+
const r = await client.get(
|
|
86
|
+
`/learn/api/public/v2/courses/${courseId}/gradebook/columns/${columnId}/attempts/${attemptId}`
|
|
87
|
+
);
|
|
88
|
+
return r.data;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function uploadFile(
|
|
92
|
+
client: AxiosInstance,
|
|
93
|
+
filePath: string
|
|
94
|
+
): Promise<string> {
|
|
95
|
+
const fileName = path.basename(filePath);
|
|
96
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
97
|
+
|
|
98
|
+
const form = new FormData();
|
|
99
|
+
form.append('file', fileBuffer, {
|
|
100
|
+
filename: fileName,
|
|
101
|
+
contentType: 'application/octet-stream',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const r = await client.post('/learn/api/public/v1/uploads', form, {
|
|
105
|
+
headers: {
|
|
106
|
+
...form.getHeaders(),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
return r.data.id as string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function submitAttempt(
|
|
113
|
+
client: AxiosInstance,
|
|
114
|
+
courseId: string,
|
|
115
|
+
columnId: string,
|
|
116
|
+
body: SubmitAttemptBody
|
|
117
|
+
): Promise<Attempt> {
|
|
118
|
+
const r = await client.post(
|
|
119
|
+
`/learn/api/public/v2/courses/${courseId}/gradebook/columns/${columnId}/attempts`,
|
|
120
|
+
{ ...body, status: body.status ?? 'NeedsGrading' }
|
|
121
|
+
);
|
|
122
|
+
return r.data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function getMyGrade(
|
|
126
|
+
client: AxiosInstance,
|
|
127
|
+
courseId: string,
|
|
128
|
+
columnId: string,
|
|
129
|
+
userId: string
|
|
130
|
+
): Promise<any> {
|
|
131
|
+
const r = await client.get(
|
|
132
|
+
`/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}/columns/${columnId}`
|
|
133
|
+
);
|
|
134
|
+
return r.data;
|
|
135
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
2
|
+
import type { Session } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
const BASE_URL = 'https://aulavirtual.upc.edu.pe';
|
|
5
|
+
|
|
6
|
+
export function createClient(session: Session): AxiosInstance {
|
|
7
|
+
// Build cookie header string
|
|
8
|
+
const cookieStr = session.cookies
|
|
9
|
+
.filter((c) => {
|
|
10
|
+
const domain = c.domain.replace(/^\./, '');
|
|
11
|
+
return 'aulavirtual.upc.edu.pe'.endsWith(domain) || domain === 'aulavirtual.upc.edu.pe';
|
|
12
|
+
})
|
|
13
|
+
.map((c) => `${c.name}=${c.value}`)
|
|
14
|
+
.join('; ');
|
|
15
|
+
|
|
16
|
+
const client = axios.create({
|
|
17
|
+
baseURL: BASE_URL,
|
|
18
|
+
headers: {
|
|
19
|
+
Cookie: cookieStr,
|
|
20
|
+
Accept: 'application/json',
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
...(session.xsrfToken ? { 'X-Blackboard-XSRF': session.xsrfToken } : {}),
|
|
23
|
+
},
|
|
24
|
+
withCredentials: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Intercept 401 to give a helpful message
|
|
28
|
+
client.interceptors.response.use(
|
|
29
|
+
(r) => r,
|
|
30
|
+
(err: AxiosError) => {
|
|
31
|
+
if (err.response?.status === 401) {
|
|
32
|
+
const e = new Error('Session expired. Run: blackboard login');
|
|
33
|
+
(e as any).code = 'SESSION_EXPIRED';
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return client;
|
|
41
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
import type { Course, UserCourse, PaginatedResponse } from '../types/index.js';
|
|
3
|
+
|
|
4
|
+
export async function getMe(client: AxiosInstance): Promise<any> {
|
|
5
|
+
const r = await client.get('/learn/api/public/v1/users/me');
|
|
6
|
+
return r.data;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function getMyCourses(
|
|
10
|
+
client: AxiosInstance,
|
|
11
|
+
userId: string,
|
|
12
|
+
opts: { limit?: number; offset?: number } = {}
|
|
13
|
+
): Promise<PaginatedResponse<UserCourse & { course?: Course }>> {
|
|
14
|
+
const params: Record<string, any> = { limit: opts.limit ?? 50 };
|
|
15
|
+
if (opts.offset) params.offset = opts.offset;
|
|
16
|
+
|
|
17
|
+
const r = await client.get(`/learn/api/public/v1/users/${userId}/courses`, { params });
|
|
18
|
+
const memberships: UserCourse[] = r.data.results;
|
|
19
|
+
|
|
20
|
+
// Resolve course names in parallel
|
|
21
|
+
const courseDetails = await Promise.allSettled(
|
|
22
|
+
memberships.map((m) => getCourse(client, m.courseId))
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const results = memberships.map((m, i) => ({
|
|
26
|
+
...m,
|
|
27
|
+
course: courseDetails[i].status === 'fulfilled' ? courseDetails[i].value : undefined,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
return { results, paging: r.data.paging };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function getCourse(client: AxiosInstance, courseId: string): Promise<Course> {
|
|
34
|
+
const r = await client.get(`/learn/api/public/v1/courses/${courseId}`);
|
|
35
|
+
return r.data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function listCourses(
|
|
39
|
+
client: AxiosInstance,
|
|
40
|
+
opts: { limit?: number; offset?: number } = {}
|
|
41
|
+
): Promise<PaginatedResponse<Course>> {
|
|
42
|
+
const r = await client.get('/learn/api/public/v1/courses', {
|
|
43
|
+
params: { limit: opts.limit ?? 50, offset: opts.offset ?? 0 },
|
|
44
|
+
});
|
|
45
|
+
return r.data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function getCourseContents(
|
|
49
|
+
client: AxiosInstance,
|
|
50
|
+
courseId: string,
|
|
51
|
+
parentId?: string
|
|
52
|
+
): Promise<PaginatedResponse<any>> {
|
|
53
|
+
const path = parentId
|
|
54
|
+
? `/learn/api/public/v1/courses/${courseId}/contents/${parentId}/children`
|
|
55
|
+
: `/learn/api/public/v1/courses/${courseId}/contents`;
|
|
56
|
+
const r = await client.get(path, {
|
|
57
|
+
params: {
|
|
58
|
+
limit: 100,
|
|
59
|
+
fields: 'id,parentId,title,body,created,modified,position,hasChildren,launchInNewWindow,availability,contentHandler',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return r.data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getCourseAnnouncements(
|
|
66
|
+
client: AxiosInstance,
|
|
67
|
+
courseId: string
|
|
68
|
+
): Promise<PaginatedResponse<any>> {
|
|
69
|
+
const r = await client.get(`/learn/api/public/v1/courses/${courseId}/announcements`, {
|
|
70
|
+
params: { limit: 20 },
|
|
71
|
+
});
|
|
72
|
+
return r.data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getGradeColumns(
|
|
76
|
+
client: AxiosInstance,
|
|
77
|
+
courseId: string
|
|
78
|
+
): Promise<PaginatedResponse<any>> {
|
|
79
|
+
const r = await client.get(`/learn/api/public/v1/courses/${courseId}/gradebook/columns`, {
|
|
80
|
+
params: { limit: 50 },
|
|
81
|
+
});
|
|
82
|
+
return r.data;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function getGrades(
|
|
86
|
+
client: AxiosInstance,
|
|
87
|
+
courseId: string,
|
|
88
|
+
userId: string
|
|
89
|
+
): Promise<PaginatedResponse<any>> {
|
|
90
|
+
const r = await client.get(
|
|
91
|
+
`/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}`,
|
|
92
|
+
{ params: { limit: 50 } }
|
|
93
|
+
);
|
|
94
|
+
return r.data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function getCourseMemberships(
|
|
98
|
+
client: AxiosInstance,
|
|
99
|
+
courseId: string
|
|
100
|
+
): Promise<PaginatedResponse<any>> {
|
|
101
|
+
const r = await client.get(`/learn/api/public/v1/courses/${courseId}/users`, {
|
|
102
|
+
params: { limit: 100 },
|
|
103
|
+
});
|
|
104
|
+
return r.data;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function getSystemVersion(client: AxiosInstance): Promise<any> {
|
|
108
|
+
const r = await client.get('/learn/api/public/v1/system/version');
|
|
109
|
+
return r.data;
|
|
110
|
+
}
|