Skip to content

Commit 3fd5b23

Browse files
committed
feat: add totp utility routes
1 parent 0b02ffc commit 3fd5b23

9 files changed

Lines changed: 70 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ For typed client generation in C++, C#, F#, D, Erlang, Elixir, Nim, Haskell, Lis
221221

222222
## Features
223223

224-
1. **Authorization** — login, 2FA, settings management
224+
1. **Authorization** — login, 2FA, TOTP utilities, settings management
225225
2. **Account** — account info, profile, profile picture, privacy
226226
3. **Media** — info, paginated comments, likes, saves, pins, archive, edit, delete
227227
4. **Direct** — inbox, threads, messages, seen state

aiograpi_rest/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ def _app_version() -> str:
130130
"patchAuthSettings": "Save auth settings",
131131
"postAuthTotp": "Enable TOTP two-factor authentication",
132132
"deleteAuthTotp": "Disable TOTP two-factor authentication",
133+
"getAuthTotpCode": "Generate a TOTP code",
134+
"getAuthTotpSeed": "Generate a TOTP seed",
133135
"postAuthChallengeResolve": "Resolve an Instagram login challenge",
134136
"getAccount": "Get authenticated account info",
135137
"getAccountArchiveMedia": "List archived account media",

aiograpi_rest/routers/auth.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from typing import Dict, List, Optional, Union
33
from unittest.mock import patch
44

5-
from fastapi import APIRouter, Depends, Form, HTTPException
5+
from aiograpi import Client
6+
from fastapi import APIRouter, Depends, Form, HTTPException, Query
67

78
from aiograpi_rest.dependencies import ClientStorage, get_clients, get_optional_sessionid, get_sessionid
89

@@ -94,6 +95,22 @@ async def auth_relogin(sessionid: str = Depends(get_sessionid),
9495
return await cl.relogin()
9596

9697

98+
@router.get("/totp/seed", response_model=str)
99+
async def totp_seed(sessionid: str = Depends(get_sessionid),
100+
clients: ClientStorage = Depends(get_clients)) -> str:
101+
"""Generate TOTP seed
102+
"""
103+
cl = await clients.get(sessionid)
104+
return await cl.totp_generate_seed()
105+
106+
107+
@router.get("/totp/code", response_model=str)
108+
async def totp_code(seed: str = Query(...)) -> str:
109+
"""Generate TOTP code
110+
"""
111+
return Client().totp_generate_code(seed)
112+
113+
97114
@router.get("/settings")
98115
async def settings_get(sessionid: str = Depends(get_sessionid),
99116
clients: ClientStorage = Depends(get_clients)) -> Dict:

docs/aiograpi-coverage.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ the installed `aiograpi.Client` class and the local FastAPI router implementatio
99
## Summary
1010

1111
- Public `aiograpi.Client` methods: **500**
12-
- Methods reached by REST routes: **269**
13-
- Methods not exposed as REST routes: **231**
14-
- Candidate REST backlog: **6**
12+
- Methods reached by REST routes: **271**
13+
- Methods not exposed as REST routes: **229**
14+
- Candidate REST backlog: **4**
1515

1616
## REST Relevance
1717

1818
| Status | Methods | Meaning |
1919
|---|---:|---|
20-
| `exposed` | 269 | Already used by public REST routes. |
21-
| `candidate` | 6 | Likely useful as a future user-facing REST endpoint. |
20+
| `exposed` | 271 | Already used by public REST routes. |
21+
| `candidate` | 4 | Likely useful as a future user-facing REST endpoint. |
2222
| `duplicate` | 123 | Variant of an already exposed method, such as `_v1`, `_gql`, `_a1`, chunk, or origin helpers. |
2323
| `internal` | 102 | Low-level auth/request/configuration/signup/challenge helpers that should not be mirrored blindly. |
2424

@@ -57,7 +57,7 @@ the installed `aiograpi.Client` class and the local FastAPI router implementatio
5757
| `signup` | 0 | 0 | 0 | 15 | 15 |
5858
| `story` | 14 | 0 | 7 | 0 | 21 |
5959
| `timeline` | 4 | 0 | 0 | 0 | 4 |
60-
| `totp` | 2 | 2 | 0 | 0 | 4 |
60+
| `totp` | 4 | 0 | 0 | 0 | 4 |
6161
| `track` | 5 | 0 | 0 | 0 | 5 |
6262
| `user` | 46 | 0 | 22 | 6 | 74 |
6363
| `video` | 5 | 0 | 1 | 4 | 10 |
@@ -69,7 +69,6 @@ the installed `aiograpi.Client` class and the local FastAPI router implementatio
6969
| `explore` | `report_explore_media` |
7070
| `fundraiser` | `standalone_fundraiser_info_v1` |
7171
| `multiple_accounts` | `featured_accounts_v1`, `get_account_family_v1` |
72-
| `totp` | `totp_generate_code`, `totp_generate_seed` |
7372

7473
## REST Routes To aiograpi Methods
7574

@@ -113,6 +112,8 @@ the installed `aiograpi.Client` class and the local FastAPI router implementatio
113112
| `PATCH /auth/settings` | `expose`, `set_locale`, `set_proxy`, `set_settings`, `set_timezone_offset` |
114113
| `DELETE /auth/totp` | `totp_disable` |
115114
| `POST /auth/totp` | `totp_enable` |
115+
| `GET /auth/totp/code` | `totp_generate_code` |
116+
| `GET /auth/totp/seed` | `totp_generate_seed` |
116117
| `GET /clip/creation/info` | `clip_info_for_creation` |
117118
| `GET /clip/download` | `clip_download` |
118119
| `GET /clip/download/by/url` | `clip_download_by_url` |
@@ -731,8 +732,8 @@ the installed `aiograpi.Client` class and the local FastAPI router implementatio
731732
| `top_search(self, query)` | `public` | - | `internal` | low-level aiograpi helper or unsafe generic surface |
732733
| `totp_disable(self) -> bool` | `totp` | `DELETE /auth/totp` | `exposed` | used by at least one public REST route |
733734
| `totp_enable(self, verification_code: str) -> List[str]` | `totp` | `POST /auth/totp` | `exposed` | used by at least one public REST route |
734-
| `totp_generate_code(seed: str) -> str` | `totp` | - | `candidate` | potential user-facing REST endpoint |
735-
| `totp_generate_seed(self) -> str` | `totp` | - | `candidate` | potential user-facing REST endpoint |
735+
| `totp_generate_code(seed: str) -> str` | `totp` | `GET /auth/totp/code` | `exposed` | used by at least one public REST route |
736+
| `totp_generate_seed(self) -> str` | `totp` | `GET /auth/totp/seed` | `exposed` | used by at least one public REST route |
736737
| `track_download_by_url(self, url: str, filename: str = '', folder: pathlib._local.Path = '') -> pathlib._local.Path` | `track` | `GET /track/download/by/url` | `exposed` | used by at least one public REST route |
737738
| `track_info_by_canonical_id(self, music_canonical_id: str) -> aiograpi.types.Track` | `track` | `GET /track` | `exposed` | used by at least one public REST route |
738739
| `track_info_by_id(self, track_id: str, max_id: str = '') -> Dict` | `track` | `GET /track` | `exposed` | used by at least one public REST route |

docs/api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ any of:
3636
If `POST /auth/login` returns `TwoFactorRequired`, retry the same endpoint with
3737
the same `username` and `password` plus `verification_code`.
3838

39+
`GET /auth/totp/seed` generates a TOTP seed for the authenticated session.
40+
`GET /auth/totp/code?seed=...` generates the current six-digit TOTP code from a
41+
seed without requiring a session.
42+
3943
If it returns `ChallengeRequired`, resolve the Instagram challenge in the
4044
account/session context first, then retry login or import a known-good saved
4145
session through `PATCH /auth/settings`.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "aiograpi-rest"
7-
version = "5.13.0"
7+
version = "5.14.0"
88
description = "RESTful API service for aiograpi"
99
readme = "README.md"
1010
requires-python = ">=3.13"

tests/test_aiograpi_coverage.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ def test_aiograpi_coverage_classifies_methods_by_rest_relevance():
167167
assert classify_method(methods["archive_stories_v1"], covered).status == "duplicate"
168168
assert classify_method(methods["sticker_tray"], covered).status == "exposed"
169169
assert classify_method(methods["users_stories_gql"], covered).status == "exposed"
170+
assert classify_method(methods["totp_generate_code"], covered).status == "exposed"
171+
assert classify_method(methods["totp_generate_seed"], covered).status == "exposed"
170172
assert classify_method(methods["media_template_v1"], covered).status == "exposed"
171173
assert classify_method(methods["photo_upload_with_music"], covered).status == "exposed"
172174
assert classify_method(methods["album_upload_with_music"], covered).status == "exposed"

tests/test_app_system.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ async def test_openapi_uses_sessionid_authorize_button_for_protected_routes():
195195
public_paths = {
196196
"/auth/login",
197197
"/auth/login/by/sessionid",
198+
"/auth/totp/code",
198199
"/health",
199200
"/ready",
200201
"/metrics",
@@ -248,6 +249,8 @@ async def test_openapi_uses_rest_http_methods():
248249
"/auth/relogin": {"patch"},
249250
"/auth/settings": {"get", "patch"},
250251
"/auth/totp": {"delete", "post"},
252+
"/auth/totp/code": {"get"},
253+
"/auth/totp/seed": {"get"},
251254
"/build": {"get"},
252255
"/clip/creation/info": {"get"},
253256
"/clip/download": {"get"},
@@ -625,6 +628,8 @@ async def test_openapi_uses_human_friendly_operation_summaries():
625628
assert paths["/auth/login/by/sessionid"]["post"]["summary"] == "Create a session from an existing session ID"
626629
assert paths["/auth/settings"]["get"]["summary"] == "Get saved auth settings"
627630
assert paths["/auth/settings"]["patch"]["summary"] == "Save auth settings"
631+
assert paths["/auth/totp/code"]["get"]["summary"] == "Generate a TOTP code"
632+
assert paths["/auth/totp/seed"]["get"]["summary"] == "Generate a TOTP seed"
628633
assert paths["/account"]["get"]["summary"] == "Get authenticated account info"
629634
assert paths["/account"]["patch"]["summary"] == "Update authenticated account profile"
630635
assert paths["/account/archive/media"]["get"]["summary"] == "List archived account media"

tests/test_auth_routes.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ async def relogin(self):
3939
self.calls.append(("relogin",))
4040
return True
4141

42+
async def totp_generate_seed(self):
43+
self.calls.append(("totp_generate_seed",))
44+
return "JBSWY3DPEHPK3PXP"
45+
4246
def get_settings(self):
4347
return self.settings
4448

@@ -305,6 +309,29 @@ async def test_relogin_awaits_aiograpi(fake_storage):
305309
assert ("relogin",) in fake_storage.created.calls
306310

307311

312+
@pytest.mark.asyncio
313+
async def test_totp_seed_awaits_aiograpi(fake_storage):
314+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
315+
response = await ac.get("/auth/totp/seed", params={"sessionid": "sid"})
316+
317+
assert response.status_code == 200
318+
assert response.json() == "JBSWY3DPEHPK3PXP"
319+
assert ("totp_generate_seed",) in fake_storage.created.calls
320+
321+
322+
@pytest.mark.asyncio
323+
async def test_totp_code_generates_six_digit_code(fake_storage):
324+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
325+
response = await ac.get(
326+
"/auth/totp/code",
327+
params={"seed": "JBSWY3DPEHPK3PXP"},
328+
)
329+
330+
assert response.status_code == 200
331+
assert response.json().isdigit()
332+
assert len(response.json()) == 6
333+
334+
308335
@pytest.mark.asyncio
309336
async def test_settings_get_returns_client_settings(fake_storage):
310337
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:

0 commit comments

Comments
 (0)