ntu-cool-cli 1.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gary Xie and ntu-cool-cli contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,505 @@
1
+ Metadata-Version: 2.4
2
+ Name: ntu-cool-cli
3
+ Version: 1.3.1
4
+ Summary: Read-only CLI for NTU Cool: course announcements, course files, handouts, and submission status.
5
+ Author-email: Gary Xie <gary.1020.xie@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Gary1020-byte/ntu-cool-cli
8
+ Project-URL: Repository, https://github.com/Gary1020-byte/ntu-cool-cli
9
+ Project-URL: Issues, https://github.com/Gary1020-byte/ntu-cool-cli/issues
10
+ Project-URL: Changelog, https://github.com/Gary1020-byte/ntu-cool-cli/blob/main/CHANGELOG.md
11
+ Keywords: ntu,cool,ceiba,cli,announcements,course,files,homework,handout,student,scraper,read-only
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Natural Language :: English
16
+ Classifier: Natural Language :: Chinese (Traditional)
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Education
25
+ Classifier: Topic :: Utilities
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: requests>=2.28
30
+ Dynamic: license-file
31
+
32
+ # ntu-cool-cli
33
+
34
+ A CLI and an agent skill for read-only access to NTU Cool: course
35
+ announcements, course files, CA handouts, and submission status.
36
+
37
+ Use it from a shell, or install it as a skill and let an agent drive it.
38
+
39
+ > 中文版: [README.zh-TW.md](README.zh-TW.md)
40
+
41
+ One CLI, four stores. Cool keeps a course's material in more than one place, and
42
+ the obvious one is often empty:
43
+
44
+ | what you want | where it actually is |
45
+ |---|---|
46
+ | what a course announced | `discussion_topics` — announcements and their attachments |
47
+ | course files: handouts, worksheets, solutions, syllabus | the Files tree (`folders`) |
48
+ | a CA handout, when the Files tree looks empty | the **assignment description** (`assignments`) |
49
+ | lectures grouped by week | **modules** (`modules` → `modules/{id}/items`) |
50
+ | whether your submission was accepted | `/submissions/self` |
51
+
52
+ ```bash
53
+ # no credentials — is the platform reachable?
54
+ python3 scripts/ntu_fetch.py --probe
55
+
56
+ # what courses am I taking this term?
57
+ python3 scripts/ntu_fetch.py --list-files --course <COURSE> --cred-file ~/.ntu-creds.json
58
+ ```
59
+
60
+ **Read-only.** It logs in to read and never writes, posts, or comments on
61
+ cool.ntu.edu.tw.
62
+
63
+ ---
64
+
65
+ ## Why not a service
66
+
67
+ A daemon that polls on a schedule and pushes to Telegram needs a persistent
68
+ store, an ORM, and a migration surface — to answer a one-endpoint question.
69
+ On Python 3.14 that stack fails three ways before it reaches the endpoint:
70
+
71
+ ```
72
+ ImportError: cannot import name 'Markup' from 'jinja2' # Flask 1.0.2
73
+ AttributeError: module 'sqlite3.dbapi2' has no attribute 'version_info' # SQLAlchemy 1.3.8
74
+ TypeError: Mapper.__init__() got an unexpected keyword argument 'order_by'
75
+ ```
76
+
77
+ Upgrading Flask cascades into the SQLAlchemy break, which cascades into a
78
+ mapper-API removal. The total patch surface is one file and four lines — which
79
+ tells you the service is the problem, not the pins.
80
+
81
+ This script drops Flask, SQLAlchemy, and `beautifulsoup4` entirely. **The only
82
+ third-party import is `requests`.** HTML parsing uses `html.parser` from the
83
+ stdlib, because the two things needed from BeautifulSoup — "find a form, read
84
+ its inputs" and "flatten visible text" — are twenty lines of `HTMLParser`.
85
+
86
+ Fetching is also the honest scope: a script that reads and exits has nothing to
87
+ keep in sync. Storage, dedupe, and delivery belong to whatever runs it.
88
+
89
+ ---
90
+
91
+ ## Install
92
+
93
+ As a command — this gives you `ntu-cool-cli` on your PATH.
94
+
95
+ ```bash
96
+ pipx install ntu-cool-cli # or: uv tool install ntu-cool-cli
97
+ ntu-cool-cli --probe
98
+ ```
99
+
100
+ Or with plain pip, into your user site:
101
+
102
+ ```bash
103
+ python3 -m pip install --user ntu-cool-cli
104
+ ```
105
+
106
+ **From Git instead** — if you want the unreleased tip, or to pin a commit:
107
+
108
+ ```bash
109
+ uv tool install 'git+https://github.com/Gary1020-byte/ntu-cool-cli.git'
110
+ pipx install 'git+https://github.com/Gary1020-byte/ntu-cool-cli.git'
111
+ ```
112
+
113
+ **From a local checkout:**
114
+
115
+ ```bash
116
+ git clone https://github.com/Gary1020-byte/ntu-cool-cli.git
117
+ cd ntu-cool-cli
118
+ pipx install . # or: uv tool install . / python3 -m pip install --user .
119
+ ntu-cool-cli --probe
120
+ ```
121
+
122
+ The module is `ntu_fetch`, the command is `ntu-cool-cli`, and both do exactly the
123
+ same thing — `ntu-cool-cli --help` and `python3 scripts/ntu_fetch.py --help` are
124
+ identical. Use whichever suits; the script needs no install at all.
125
+
126
+ Requires Python 3.9+ and `requests`. Verify:
127
+
128
+ ```bash
129
+ ntu-cool-cli --probe
130
+ ```
131
+
132
+ ### Tests
133
+
134
+ ```bash
135
+ python3 -m unittest discover -s tests -v
136
+ ```
137
+
138
+ 52 offline tests, no network and no credentials. They cover the JSON prefix
139
+ handling, HTML form extraction without BeautifulSoup, the credential-file
140
+ permission guard, and — the one that matters — that a trap form parked in
141
+ `<head>` can never become a POST target.
142
+
143
+ Two classes guard the bug that was silently losing files: one drives
144
+ `_paged_get` across a multi-page Link chain, one proves the walk enters through
145
+ the root and still works when `/folders/root` answers 403.
146
+
147
+ ### Note
148
+
149
+ The console script is a thin wrapper: `ntu_fetch:main`, with `main()` returning
150
+ the exit code. Importing the module has no side effects, so the tests can import
151
+ it without logging in or reaching the network.
152
+
153
+ > **Test with the interpreter you will actually use.** A virtualenv's Python and the
154
+ > system Python have separate `site-packages`. A bug report that says "the import
155
+ > fails" is often a report about the wrong interpreter.
156
+ > See `references/testing-and-interpreters.md`.
157
+
158
+ ---
159
+
160
+ ## Credentials
161
+
162
+ Two platforms need your NTU account. **This tool never stores, requests, or prints a
163
+ password.**
164
+
165
+ Either export them for one shell:
166
+
167
+ ```bash
168
+ export NTU_USER=bXXXXXXXX
169
+ export NTU_PASS='...'
170
+ ```
171
+
172
+ …or, better, keep them in a file the script will only read when it is mode `600`:
173
+
174
+ ```bash
175
+ umask 077
176
+ echo '{"ntu_user":"bXXXXXXXX","ntu_pass":"..."}' > ~/.ntu-creds.json
177
+ ```
178
+
179
+ A group- or world-readable credential file is **refused** with the `chmod` command to
180
+ fix it. Note that environment variables exported in one terminal are not visible to
181
+ another — a credential file is what makes this script usable unattended (cron, etc.).
182
+
183
+ ### Where the password can and cannot leak
184
+
185
+ The password is submitted to ADFS as a form field. Four things it could have
186
+ done, and what it actually does:
187
+
188
+ | leak surface | what the tool does |
189
+ |---|---|
190
+ | printed in an error message | it doesn't — the login failure names the hop and the HTTP status, never the submitted values |
191
+ | dumped by `requests` debug logging | it doesn't — `enable_http_debugging()` and `debuglevel` are absent, and a test asserts they stay absent |
192
+ | left in the output as a traceback | it doesn't — the traceback would carry the *call*, not the value; the value only ever exists in the POST body |
193
+ | sitting in a world-readable file | it refuses to read one |
194
+
195
+ **The real risks are on your side of the boundary:**
196
+
197
+ - **The credential file's path.** `--cred-file ~/.ntu-creds.json` goes into
198
+ your shell history, and the path is visible in the process list. That is
199
+ why the environment-variable path is the better one when you are typing
200
+ the command by hand — and the file path is the better one when a cron job
201
+ needs it unattended.
202
+ - **The signed download URLs.** A file's `url` from the API authenticates.
203
+ The tool never logs one, and it never writes one into a file it produces.
204
+ Don't paste one into an issue.
205
+ - **The session cookies.** They live in the `requests.Session` for the length
206
+ of the run and are gone when the process exits. Nothing is written to disk.
207
+
208
+ ---
209
+
210
+ ## The three traps this encodes
211
+
212
+ ### 1. `/api/v1/courses` is not your current course list
213
+
214
+ It returns courses the account has **ever** been enrolled in.
215
+
216
+ ```
217
+ API said: 10 courses, newest announcement 2026-10-13, oldest 2025-09-05
218
+ → omitted 3 current-term courses, one of them your main math course
219
+ → included 3 dead courses as {"access_restricted_by_date": true}
220
+
221
+ Home page says: 7 courses, term "<term>" ← correct
222
+ ```
223
+
224
+ `fetch_courses()` parses the roster out of the home page's embedded JSON. The
225
+ historical list is still reachable via `fetch_all_courses_historical()` when you want
226
+ last term's exam schedule — never as the current roster.
227
+
228
+ ### 2. Course material is not in the announcement body
229
+
230
+ Measured on one term:
231
+
232
+ ```
233
+ 20 announcement bodies 2,041 characters 0 file links
234
+ course Files tree 91 files
235
+ announcement attachments 5 files <- a third set
236
+ ```
237
+
238
+ An announcement whose entire body is the word `attached` is **not** a missing-
239
+ attachment case to report — it is the normal shape, and the files are fully
240
+ retrievable. Reporting "this course has no attachments" after reading announcements
241
+ alone reads as complete and misses every worksheet.
242
+
243
+ ### 3. Enter through the root, and follow `Link` — three separate traps
244
+
245
+ ```
246
+ GET /api/v1/courses/{id}/folders/root → the root folder <- enter here
247
+ folder["files_url"] → files in that folder (paginated)
248
+ folder["folders_url"] → child folders; recurse (paginated)
249
+ ```
250
+
251
+ **(a) The URLs come off the objects.** Six plausible spellings return 404 or 400 —
252
+ including `/api/v1/courses/{id}/files`, which looks obviously right and is dead.
253
+
254
+ **(b) `/folders` is a flat list, not a tree.** It enumerates folders at every depth.
255
+ Iterating it *and* recursing `folders_url` walks each non-root folder twice — three
256
+ times when its parent is listed too. A naive walk reported **110 files where 64 were
257
+ retrievable**. Enter through the root instead.
258
+
259
+ **(c) Every list endpoint is paginated at 10 by default.** This is the trap that
260
+ actually loses data: a folder holding 29 files returns 10, with no error and no
261
+ empty-looking tail. Measured across one term — **27 of 91 files silently missing**.
262
+
263
+ ```
264
+ projected from Link header: <...>; rel="next", <...>; rel="last"
265
+ bare request: page=1&per_page=10
266
+ ```
267
+
268
+ Follow `Link: rel="next"`. Do not "fix" it with a bigger `per_page` — the API caps
269
+ that parameter, and the Link header is the only pagination contract it honours.
270
+
271
+ `fetch_course_files()` enters through `/folders/root` (falling back to the
272
+ parentless entries of `/folders`, because `/folders/root` answers **403** on at
273
+ least one non-credit course), paginates both directions, and dedupes by file `id`
274
+ as a last resort. There are tests that fail if any of the three is removed.
275
+
276
+ Announcement attachments are a **third** set, separate from both: one term had 5 of
277
+ them and 91 in the Files tree. `--list-files` and `--download-files` collect both.
278
+
279
+ ---
280
+
281
+ ## Usage
282
+
283
+ ### Announcements
284
+
285
+ ```bash
286
+ # current term roster
287
+ python3 scripts/ntu_fetch.py --list-files --course <COURSE> --cred-file ~/.ntu-creds.json
288
+
289
+ # one course's announcements to stdout
290
+ python3 scripts/ntu_fetch.py --course <COURSE> --since 2026-09-01 --stdout
291
+
292
+ # write markdown into a notes directory
293
+ python3 scripts/ntu_fetch.py --course <COURSE> --since 2026-09-01 --out ./captures/
294
+ ```
295
+
296
+ ### Course files
297
+
298
+ ```bash
299
+ # list everything in the Files tree
300
+ python3 scripts/ntu_fetch.py --list-files --course <COURSE> --cred-file ~/.ntu-creds.json
301
+
302
+ # download, skipping anything over 20 MB
303
+ python3 scripts/ntu_fetch.py --download-files --course <COURSE> \
304
+ --cred-file ~/.ntu-creds.json --files-out ./files/ --max-mb 20
305
+ ```
306
+
307
+ Downloads land at `<files-out>/<course_id>/<file_id>/<filename>`, so two files with
308
+ the same display name never collide.
309
+
310
+ ### Assignments, submissions, pages, modules
311
+
312
+ ```bash
313
+ # the homework roster: name, due_at, lock_at, points
314
+ python3 scripts/ntu_fetch.py --list-assignments --course <COURSE> --cred-file ~/.ntu-creds.json
315
+
316
+ # your own submission status per assignment (read-only)
317
+ python3 scripts/ntu_fetch.py --list-submissions --course <COURSE> --cred-file ~/.ntu-creds.json
318
+
319
+ # course pages: syllabus, calendar, late-submission policy
320
+ python3 scripts/ntu_fetch.py --list-pages --course <COURSE> --cred-file ~/.ntu-creds.json
321
+
322
+ # modules — lectures grouped by week
323
+ python3 scripts/ntu_fetch.py --list-modules --course <COURSE> --cred-file ~/.ntu-creds.json
324
+
325
+ # handout links that live in assignment descriptions, not the Files tree
326
+ python3 scripts/ntu_fetch.py --list-handouts --course <COURSE> --cred-file ~/.ntu-creds.json
327
+
328
+ # one course page's content, by slug (see --list-pages for the slugs)
329
+ python3 scripts/ntu_fetch.py --show-page ke-cheng-da-gang --course <COURSE> \
330
+ --cred-file ~/.ntu-creds.json
331
+
332
+ # download the handouts themselves
333
+ python3 scripts/ntu_fetch.py --download-handout --course <COURSE> \
334
+ --cred-file ~/.ntu-creds.json --files-out ./handouts/
335
+ ```
336
+
337
+ `lock_at` is the real deadline; `due_at` is the nominal one, and the gap between
338
+ them is the buffer the course page promises. A `lock_at` in the past means the
339
+ upload window is closed.
340
+
341
+ `--show-page` needs exactly one course — it prints the page's title, its URL,
342
+ and the body as plain text. `--download-handout` follows the two-hop path
343
+ (the endpoint answers with JSON describing the file; the signed URL is inside
344
+ it) and writes the file under its real display name.
345
+
346
+ ---
347
+
348
+ ## Installing this as an agent skill
349
+
350
+ Copy the block below into your agent (Claude Code, Hermes, a custom assistant —
351
+ anything that can run a command and read a file).
352
+
353
+ **The agent must ask which capabilities are needed, then install only those.**
354
+ Each capability maps to a specific flag or a specific section of `SKILL.md`, so
355
+ a partial install is the normal case, not a compromise.
356
+
357
+ ````text
358
+ Install the NTU Cool skill: https://github.com/Gary1020-byte/ntu-cool-cli
359
+
360
+ Before installing anything, ask me which capabilities I need. Do NOT install
361
+ all of them by default. The capabilities are:
362
+
363
+ 1. Announcements — course announcements, deadlines, TA notices
364
+ flag: --course / --all-courses
365
+ 2. Course files — the Files tree: handouts, worksheets, solutions,
366
+ syllabus PDFs
367
+ flags: --list-files, --download-files
368
+ 3. Announcement attachments — files bound to one announcement (a separate,
369
+ smaller set from the Files tree)
370
+ included in --list-files / --download-files
371
+ 4. Homework roster — /assignments: what is assigned, due_at, lock_at,
372
+ points, and the handout link
373
+ flag: --list-assignments
374
+ 5. Submission status — my own submission per assignment: unsubmitted /
375
+ submitted / graded, score, late policy
376
+ flag: --list-submissions
377
+ 6. Course pages — syllabus, calendar, late-submission policy,
378
+ submission instructions
379
+ flag: --list-pages
380
+ 7. Modules — lectures grouped by week
381
+ flag: --list-modules
382
+ 8. CA handouts — handout links that live in assignment descriptions
383
+ rather than the Files tree (the Files tree is often
384
+ nearly empty for CA courses)
385
+ flags: --list-handouts, --download-handout
386
+ 9. Page content — the body of one course page: syllabus, calendar,
387
+ late-submission policy, submission instructions
388
+ flag: --show-page <slug>
389
+
390
+ Show me this list, ask which ones I want, and wait for my answer.
391
+
392
+ Then:
393
+ - clone the repo
394
+ - install the `ntu-cool-cli` package:
395
+ `pipx install ntu-cool-cli` (or `uv tool install ntu-cool-cli`, or
396
+ `python3 -m pip install --user ntu-cool-cli`). It is on PyPI. If the
397
+ user wants the unreleased tip instead, install from the Git URL:
398
+ `uv tool install 'git+https://github.com/Gary1020-byte/ntu-cool-cli.git'`
399
+ - read SKILL.md, and keep only the sections matching the capabilities I chose
400
+ - tell me how to supply credentials. Prefer the environment variables
401
+ NTU_USER and NTU_PASS — nothing lands on disk and nothing stays in the
402
+ shell history. The alternative is a JSON file with ntu_user / ntu_pass
403
+ keys, passed with --cred-file; that file must be mode 600, because the
404
+ tool refuses to read a world-readable one. Never write credentials into
405
+ the repo, and never echo them back to me.
406
+ - run `ntu-cool-cli --probe` first. It needs no credentials and tells you
407
+ whether the platform is reachable. Do not debug credentials until the
408
+ probe is clean.
409
+
410
+ This tool is READ-ONLY. It logs in to read and never writes, posts, or
411
+ comments on cool.ntu.edu.tw. It cannot upload a submission — if I need to
412
+ hand something in, tell me what is due and where the file is, and let me do
413
+ the upload in the browser. Never attempt a write against the platform.
414
+ ````
415
+
416
+ ### Why the agent should ask first
417
+
418
+ Cool keeps a course's material in four different places, and which one holds
419
+ what is not guessable from the outside — the Files tree is nearly empty for
420
+ some CA courses, and a handout that is missing from it is usually in the
421
+ assignment description. Installing all nine capabilities means the agent
422
+ reads a skill file that describes stores it will never query, and every
423
+ capability it does not need is a way for it to answer a question with the
424
+ wrong source.
425
+
426
+ The list also doubles as a map: an agent that knows "the Files tree is empty
427
+ for this course" knows to ask for `--list-handouts` instead of reporting
428
+ "no attachments".
429
+
430
+ ---
431
+
432
+ ## What the output is worth trusting
433
+
434
+ | claim | status |
435
+ |---|---|
436
+ | NTU Cool endpoints still exist | verified — 401 + structured body, not 404 |
437
+ | the SAML login sequence | verified against the live site |
438
+ | `/api/v1/courses` as current roster | **false** — see trap 1 |
439
+ | the Files-tree endpoint shape | verified, 91 files enumerated across a term |
440
+ | downloads are really PDFs | magic-checked per file |
441
+ | nothing is silently dropped | verified — pagination followed via `Link` |
442
+ | Ceiba reachability | **varies by network** — see `references/ceiba-unreachable.md` |
443
+ | Ceiba scraping | not implemented; the positional table parser is unverified |
444
+
445
+ **A download reporting `OK` has passed both checks.** Cool answers an expired signed
446
+ URL with HTTP 200 carrying the login page, so status code alone proves nothing —
447
+ the script sniffs for `<!DOCTYPE html` and verifies `%PDF-` against the declared
448
+ content type.
449
+
450
+ Signed file URLs are credential-bearing. The script never logs them; don't paste them
451
+ anywhere.
452
+
453
+ ---
454
+
455
+ ## Many course PDFs have no text layer
456
+
457
+ Of 10 files in one course, **9 had a zero-length text layer** — they are rasterised
458
+ scans or vector drawings. That is not data loss. Render at ~160 dpi and read the
459
+ image:
460
+
461
+ ```python
462
+ import pymupdf
463
+ d = pymupdf.open("worksheet.pdf")
464
+ d[0].get_pixmap(dpi=160).save("page1.png") # then read it
465
+ ```
466
+
467
+ Never file a PDF as unusable on the strength of one extraction path.
468
+
469
+ ---
470
+
471
+ ## Ceiba
472
+
473
+ `ceiba.ntu.edu.tw` is not implemented. On some networks it is unreachable (DNS
474
+ resolves, three sibling NTU hosts answer in <150 ms, ceiba times out on connect).
475
+ That is a network-access question, not a code one — see
476
+ `references/ceiba-unreachable.md` for the control test that distinguishes them.
477
+
478
+ ---
479
+
480
+ ## Files
481
+
482
+ ```
483
+ SKILL.md the rulebook: verification status, traps, workflow
484
+ scripts/ntu_fetch.py the whole tool
485
+ pyproject.toml makes `ntu-cool-cli` installable
486
+ tests/test_ntu_fetch.py 52 offline tests, no network
487
+ ACKNOWLEDGEMENTS.md attribution and licensing notes
488
+ references/api-findings.md what the platform actually returns
489
+ references/ceiba-unreachable.md reachability control test
490
+ references/testing-and-interpreters.md the venv-vs-system-python trap
491
+ ```
492
+
493
+ `SKILL.md` is written for an AI agent reading it before acting. It is readable by
494
+ humans too, but its shape is "what is verified, what is not, what will bite you".
495
+
496
+ ---
497
+
498
+ ## Ethics and scope
499
+
500
+ Read-only. Every request this script makes is a `GET` except the two login form
501
+ submissions required to obtain a session — there is no upload, no write, no delete,
502
+ and no course modification of any kind. It obeys the same rule as the account
503
+ holder: fetch what you can already see by logging in, and nothing more.
504
+
505
+ Use it against your own account.