LINE Client Skill
Full LINE messaging client via the Chrome extension gateway JSON API.
Repo & Files
- Repo:
/data/workspace/line-client (github.com/2manslkh/line-api)
- Main client:
src/chrome_client.py → LineChromeClient
- QR login:
src/auth/qr_login.py → QRLogin
- HMAC signer:
src/hmac/signer.js (Node.js, auto-starts on port 18944)
- Token storage:
~/.line-client/tokens.json
- Certificate cache:
~/.line-client/sqr_cert
- WASM files:
lstm.wasm + lstmSandbox.js (required, in repo root)
Quick Start
import json
from pathlib import Path
from src.chrome_client import LineChromeClient
tokens = json.loads((Path.home() / ".line-client" / "tokens.json").read_text())
client = LineChromeClient(auth_token=tokens["auth_token"])
# Send a message
client.send_message("U...", "Hello!")
# Get profile
profile = client.get_profile()
Tokens expire in ~7 days. If expired (APIError(10051)), re-run QR login.
QR Login (Authentication)
QR login requires user interaction: scan QR on phone + enter PIN.
from src.hmac import HmacSigner
from src.auth.qr_login import QRLogin
import qrcode
signer = HmacSigner(mode="server")
login = QRLogin(signer)
result = login.run(
url: send_qr_image_to_user(qrcode.make(url)),
pin: send_pin_to_user_IMMEDIATELY(pin), # TIME SENSITIVE!
msg: print(msg),
)
# result.auth_token, result.mid, result.refresh_token
Critical: The PIN must reach the user within ~60 seconds. Send it the instant on_pin fires.
QR Login State Machine
createSession → session ID
createQrCode → callback URL (append ?secret={curve25519_pubkey}&e2eeVersion=1)
checkQrCodeVerified — poll until scan (uses X-Line-Session-ID, no origin header)
verifyCertificate — MUST be called even if it fails (required state transition!)
createPinCode → 6-digit PIN (skip if cert verified in step 4)
checkPinCodeVerified — poll until user enters PIN
qrCodeLoginV2 → JWT token + certificate + refresh token
Server-Side Login Script
python scripts/qr_login_server.py /tmp/qr.png
Emits JSON events on stdout: {"event": "qr", "path": "...", "url": "..."}, {"event": "pin", "pin": "123456"}, {"event": "done", "mid": "U..."}.
All API Methods
Contacts & Friends
| Method |
Args |
Description |
get_profile() |
— |
Get your own profile (displayName, mid, statusMessage, etc.) |
get_contact(mid) |
mid: str |
Get a single contact's profile |
get_contacts(mids) |
mids: list[str] |
Get multiple contacts |
get_all_contact_ids() |
— |
List all friend MIDs |
find_contact_by_userid(userid) |
userid: str |
Search by LINE ID |
find_and_add_contact_by_mid(mid) |
mid: str |
Add friend by MID |
find_contacts_by_phone(phones) |
phones: list[str] |
Search by phone numbers |
add_friend_by_mid(mid) |
mid: str |
Add friend (RelationService) |
get_blocked_contact_ids() |
— |
List blocked MIDs |
get_blocked_recommendation_ids() |
— |
List blocked recommendations |
block_contact(mid) |
mid: str |
Block a contact |
unblock_contact(mid) |
mid: str |
Unblock a contact |
block_recommendation(mid) |
mid: str |
Block a friend suggestion |
update_contact_setting(mid, flag, value) |
mid, flag: int, value: str |
Update contact setting (e.g. mute) |
get_favorite_mids() |
— |
List favorited contact MIDs |
get_recommendation_ids() |
— |
List friend suggestions |
Messages
| Method |
Args |
Description |
send_message(to, text, ...) |
to: str, text: str, reply_to: str (opt) |
Send a text message. Supports replies via reply_to=message_id |
unsend_message(message_id) |
message_id: str |
Unsend/delete a sent message |
get_recent_messages(chat_id, count=50) |
chat_id: str |
Get latest messages in a chat |
get_previous_messages(chat_id, end_seq, count=50) |
chat_id, end_seq: int |
Paginated history (older messages) |
get_messages_by_ids(message_ids) |
message_ids: list[str] |
Fetch specific messages |
get_message_boxes(count=50) |
— |
Get chat list with last message (inbox view) |
get_message_boxes_by_ids(chat_ids) |
chat_ids: list[str] |
Get specific chats with last message |
get_message_read_range(chat_ids) |
chat_ids: list[str] |
Get read receipt info |
send_chat_checked(chat_id, last_message_id) |
chat_id, last_message_id: str |
Mark messages as read |
send_chat_removed(chat_id, last_message_id) |
chat_id, last_message_id: str |
Remove chat from inbox |
send_postback(to, postback_data) |
to, postback_data: str |
Send postback (bot interactions) |
Chats & Groups
| Method |
Args |
Description |
get_chats(chat_ids, with_members=True, with_invitees=True) |
chat_ids: list[str] |
Get chat/group details |
get_all_chat_mids() |
— |
List all chat MIDs (groups + invites) |
create_chat(name, target_mids) |
name: str, target_mids: list[str] |
Create a new group chat |
accept_chat_invitation(chat_id) |
chat_id: str |
Accept group invite |
reject_chat_invitation(chat_id) |
chat_id: str |
Reject group invite |
invite_into_chat(chat_id, mids) |
chat_id: str, mids: list[str] |
Invite users to group |
cancel_chat_invitation(chat_id, mids) |
chat_id: str, mids: list[str] |
Cancel pending invites |
delete_other_from_chat(chat_id, mids) |
chat_id: str, mids: list[str] |
Kick members from group |
leave_chat(chat_id) |
chat_id: str |
Leave a group chat |
update_chat(chat_id, updates) |
chat_id: str, updates: dict |
Update group name/settings |
set_chat_hidden_status(chat_id, hidden) |
chat_id: str, hidden: bool |
Archive/unarchive a chat |
get_rooms(room_ids) |
room_ids: list[str] |
Get legacy room info |
invite_into_room(room_id, mids) |
room_id: str, mids: list[str] |
Invite to legacy room |
leave_room(room_id) |
room_id: str |
Leave legacy room |
Reactions
| Method |
Args |
Description |
react(message_id, reaction_type) |
message_id: str, type: int |
React to a message. Types: 2=like, 3=love, 4=laugh, 5=surprised, 6=sad, 7=angry |
cancel_reaction(message_id) |
message_id: str |
Remove your reaction |
Profile & Settings
| Method |
Args |
Description |
update_profile_attributes(attr, value, meta={}) |
attr: int, value: str |
Update profile. Attrs: 2=DISPLAY_NAME, 16=STATUS_MESSAGE, 4=PICTURE_STATUS |
update_status_message(message) |
message: str |
Shortcut: update status message |
update_display_name(name) |
name: str |
Shortcut: update display name |
get_settings() |
— |
Get all account settings |
get_settings_attributes(attr_bitset) |
attr_bitset: int |
Get specific settings |
update_settings_attributes(attr_bitset, settings) |
attr_bitset: int, settings: dict |
Update settings |
Polling & Events
| Method |
Args |
Description |
get_last_op_revision() |
— |
Get latest operation revision number |
fetch_ops(count=50) |
— |
Fetch pending operations (may long-poll) |
poll() |
— |
Generator yielding operations as they arrive |
on_message(handler) |
handler: Callable(msg, client) |
Start polling thread, calls handler on new messages. Op types: 26=SEND_MESSAGE, 27=RECEIVE_MESSAGE |
stop() |
— |
Stop the polling thread |
Other Services
| Method |
Args |
Description |
get_server_time() |
— |
Get LINE server timestamp |
get_configurations() |
— |
Get server configurations |
get_rsa_key_info() |
— |
Get RSA key for auth |
issue_channel_token(channel_id) |
channel_id: str |
Issue channel token (LINE Login/LIFF) |
get_buddy_detail(mid) |
mid: str |
Get official account info |
report_abuse(mid, category=0, reason="") |
mid: str |
Report a user |
add_friend_by_mid(mid) |
mid: str |
Add friend (RelationService) |
logout() |
— |
Logout and invalidate token |
MID Format
LINE identifies entities by MID:
U... or u... → User (toType=0)
C... or c... → Group chat (toType=2)
R... or r... → Room (toType=1)
The client auto-detects toType from the MID prefix when sending messages.
HMAC Signing
All API calls require X-Hmac header. The WASM signer handles this automatically:
- Derives key from version "3.7.1" + access token via proprietary KDF (in lstm.wasm)
- Signs
path + body → base64 → X-Hmac
- Server mode: ~13ms/sign (Node.js HTTP server on port 18944, auto-started)
- Subprocess mode: ~2s/sign (fallback)
Error Handling
from src.chrome_client import APIError
try:
client.send_message(mid, "test")
except APIError as e:
print(e.code, e.api_message)
# 10051 = session expired / invalid
# 10052 = HTTP error from backend
# 10102 = invalid arguments
Architecture
User's Phone (LINE app)
↕ (scan QR / enter PIN)
LINE Servers (line-chrome-gw.line-apps.com)
↕ (JSON REST + X-Hmac signing)
LineChromeClient (this repo)
↕ (WASM HMAC via Node.js signer)
lstm.wasm + lstmSandbox.js
The Chrome Gateway translates JSON ↔ Thrift internally. We never deal with Thrift binary — everything is clean JSON.
1---2name: line-client3description: LINE messaging integration via Chrome extension gateway. Send/read LINE messages, manage contacts, groups, profile, and reactions. Authenticate with QR code login. Provides HMAC-signed API access through the Chrome extension gateway (line-chrome-gw.line-apps.com).4---5
6# LINE Client Skill
7
8Full LINE messaging client via the Chrome extension gateway JSON API.
9
10## Repo & Files
11
12- **Repo:** `/data/workspace/line-client` ([github.com/2manslkh/line-api](https://github.com/2manslkh/line-api))
13- **Main client:** `src/chrome_client.py` → `LineChromeClient`
14- **QR login:** `src/auth/qr_login.py` → `QRLogin`
15- **HMAC signer:** `src/hmac/signer.js` (Node.js, auto-starts on port 18944)
16- **Token storage:** `~/.line-client/tokens.json`
17- **Certificate cache:** `~/.line-client/sqr_cert`
18- **WASM files:** `lstm.wasm` + `lstmSandbox.js` (required, in repo root)
19
20## Quick Start
21
22```python
23import json
24from pathlib import Path
25from src.chrome_client import LineChromeClient
26
27tokens = json.loads((Path.home() / ".line-client" / "tokens.json").read_text())
28client = LineChromeClient(auth_token=tokens["auth_token"])
29
30# Send a message
31client.send_message("U...", "Hello!")
32
33# Get profile
34profile = client.get_profile()
35```
36
37Tokens expire in ~7 days. If expired (`APIError(10051)`), re-run QR login.
38
39## QR Login (Authentication)
40
41QR login requires user interaction: scan QR on phone + enter PIN.
42
43```python
44from src.hmac import HmacSigner
45from src.auth.qr_login import QRLogin
46import qrcode
47
48signer = HmacSigner(mode="server")
49login = QRLogin(signer)
50result = login.run(
51 on_qr=lambda url: send_qr_image_to_user(qrcode.make(url)),
52 on_pin=lambda pin: send_pin_to_user_IMMEDIATELY(pin), # TIME SENSITIVE!
53 on_status=lambda msg: print(msg),
54)
55# result.auth_token, result.mid, result.refresh_token
56```
57
58**Critical:** The PIN must reach the user within ~60 seconds. Send it the instant `on_pin` fires.
59
60### QR Login State Machine
611. `createSession` → session ID
622. `createQrCode` → callback URL (append `?secret={curve25519_pubkey}&e2eeVersion=1`)
633. `checkQrCodeVerified` — poll until scan (uses `X-Line-Session-ID`, no `origin` header)
644. **`verifyCertificate`** — MUST be called even if it fails (required state transition!)
655. `createPinCode` → 6-digit PIN (skip if cert verified in step 4)
666. `checkPinCodeVerified` — poll until user enters PIN
677. `qrCodeLoginV2` → JWT token + certificate + refresh token
68
69### Server-Side Login Script
70```bash
71python scripts/qr_login_server.py /tmp/qr.png
72```
73Emits JSON events on stdout: `{"event": "qr", "path": "...", "url": "..."}`, `{"event": "pin", "pin": "123456"}`, `{"event": "done", "mid": "U..."}`.
74
75## All API Methods
76
77### Contacts & Friends
78
79| Method | Args | Description |
80|--------|------|-------------|
81| `get_profile()` | — | Get your own profile (displayName, mid, statusMessage, etc.) |
82| `get_contact(mid)` | mid: str | Get a single contact's profile |
83| `get_contacts(mids)` | mids: list[str] | Get multiple contacts |
84| `get_all_contact_ids()` | — | List all friend MIDs |
85| `find_contact_by_userid(userid)` | userid: str | Search by LINE ID |
86| `find_and_add_contact_by_mid(mid)` | mid: str | Add friend by MID |
87| `find_contacts_by_phone(phones)` | phones: list[str] | Search by phone numbers |
88| `add_friend_by_mid(mid)` | mid: str | Add friend (RelationService) |
89| `get_blocked_contact_ids()` | — | List blocked MIDs |
90| `get_blocked_recommendation_ids()` | — | List blocked recommendations |
91| `block_contact(mid)` | mid: str | Block a contact |
92| `unblock_contact(mid)` | mid: str | Unblock a contact |
93| `block_recommendation(mid)` | mid: str | Block a friend suggestion |
94| `update_contact_setting(mid, flag, value)` | mid, flag: int, value: str | Update contact setting (e.g. mute) |
95| `get_favorite_mids()` | — | List favorited contact MIDs |
96| `get_recommendation_ids()` | — | List friend suggestions |
97
98### Messages
99
100| Method | Args | Description |
101|--------|------|-------------|
102| `send_message(to, text, ...)` | to: str, text: str, reply_to: str (opt) | Send a text message. Supports replies via `reply_to=message_id` |
103| `unsend_message(message_id)` | message_id: str | Unsend/delete a sent message |
104| `get_recent_messages(chat_id, count=50)` | chat_id: str | Get latest messages in a chat |
105| `get_previous_messages(chat_id, end_seq, count=50)` | chat_id, end_seq: int | Paginated history (older messages) |
106| `get_messages_by_ids(message_ids)` | message_ids: list[str] | Fetch specific messages |
107| `get_message_boxes(count=50)` | — | Get chat list with last message (inbox view) |
108| `get_message_boxes_by_ids(chat_ids)` | chat_ids: list[str] | Get specific chats with last message |
109| `get_message_read_range(chat_ids)` | chat_ids: list[str] | Get read receipt info |
110| `send_chat_checked(chat_id, last_message_id)` | chat_id, last_message_id: str | Mark messages as read |
111| `send_chat_removed(chat_id, last_message_id)` | chat_id, last_message_id: str | Remove chat from inbox |
112| `send_postback(to, postback_data)` | to, postback_data: str | Send postback (bot interactions) |
113
114### Chats & Groups
115
116| Method | Args | Description |
117|--------|------|-------------|
118| `get_chats(chat_ids, with_members=True, with_invitees=True)` | chat_ids: list[str] | Get chat/group details |
119| `get_all_chat_mids()` | — | List all chat MIDs (groups + invites) |
120| `create_chat(name, target_mids)` | name: str, target_mids: list[str] | Create a new group chat |
121| `accept_chat_invitation(chat_id)` | chat_id: str | Accept group invite |
122| `reject_chat_invitation(chat_id)` | chat_id: str | Reject group invite |
123| `invite_into_chat(chat_id, mids)` | chat_id: str, mids: list[str] | Invite users to group |
124| `cancel_chat_invitation(chat_id, mids)` | chat_id: str, mids: list[str] | Cancel pending invites |
125| `delete_other_from_chat(chat_id, mids)` | chat_id: str, mids: list[str] | Kick members from group |
126| `leave_chat(chat_id)` | chat_id: str | Leave a group chat |
127| `update_chat(chat_id, updates)` | chat_id: str, updates: dict | Update group name/settings |
128| `set_chat_hidden_status(chat_id, hidden)` | chat_id: str, hidden: bool | Archive/unarchive a chat |
129| `get_rooms(room_ids)` | room_ids: list[str] | Get legacy room info |
130| `invite_into_room(room_id, mids)` | room_id: str, mids: list[str] | Invite to legacy room |
131| `leave_room(room_id)` | room_id: str | Leave legacy room |
132
133### Reactions
134
135| Method | Args | Description |
136|--------|------|-------------|
137| `react(message_id, reaction_type)` | message_id: str, type: int | React to a message. Types: 2=like, 3=love, 4=laugh, 5=surprised, 6=sad, 7=angry |
138| `cancel_reaction(message_id)` | message_id: str | Remove your reaction |
139
140### Profile & Settings
141
142| Method | Args | Description |
143|--------|------|-------------|
144| `update_profile_attributes(attr, value, meta={})` | attr: int, value: str | Update profile. Attrs: 2=DISPLAY_NAME, 16=STATUS_MESSAGE, 4=PICTURE_STATUS |
145| `update_status_message(message)` | message: str | Shortcut: update status message |
146| `update_display_name(name)` | name: str | Shortcut: update display name |
147| `get_settings()` | — | Get all account settings |
148| `get_settings_attributes(attr_bitset)` | attr_bitset: int | Get specific settings |
149| `update_settings_attributes(attr_bitset, settings)` | attr_bitset: int, settings: dict | Update settings |
150
151### Polling & Events
152
153| Method | Args | Description |
154|--------|------|-------------|
155| `get_last_op_revision()` | — | Get latest operation revision number |
156| `fetch_ops(count=50)` | — | Fetch pending operations (may long-poll) |
157| `poll()` | — | Generator yielding operations as they arrive |
158| `on_message(handler)` | handler: Callable(msg, client) | Start polling thread, calls handler on new messages. Op types: 26=SEND_MESSAGE, 27=RECEIVE_MESSAGE |
159| `stop()` | — | Stop the polling thread |
160
161### Other Services
162
163| Method | Args | Description |
164|--------|------|-------------|
165| `get_server_time()` | — | Get LINE server timestamp |
166| `get_configurations()` | — | Get server configurations |
167| `get_rsa_key_info()` | — | Get RSA key for auth |
168| `issue_channel_token(channel_id)` | channel_id: str | Issue channel token (LINE Login/LIFF) |
169| `get_buddy_detail(mid)` | mid: str | Get official account info |
170| `report_abuse(mid, category=0, reason="")` | mid: str | Report a user |
171| `add_friend_by_mid(mid)` | mid: str | Add friend (RelationService) |
172| `logout()` | — | Logout and invalidate token |
173
174## MID Format
175
176LINE identifies entities by MID:
177- `U...` or `u...` → User (toType=0)
178- `C...` or `c...` → Group chat (toType=2)
179- `R...` or `r...` → Room (toType=1)
180
181The client auto-detects `toType` from the MID prefix when sending messages.
182
183## HMAC Signing
184
185All API calls require `X-Hmac` header. The WASM signer handles this automatically:
186- Derives key from version "3.7.1" + access token via proprietary KDF (in lstm.wasm)
187- Signs `path + body` → base64 → `X-Hmac`
188- Server mode: ~13ms/sign (Node.js HTTP server on port 18944, auto-started)
189- Subprocess mode: ~2s/sign (fallback)
190
191## Error Handling
192
193```python
194from src.chrome_client import APIError
195
196try:
197 client.send_message(mid, "test")
198except APIError as e:
199 print(e.code, e.api_message)
200 # 10051 = session expired / invalid
201 # 10052 = HTTP error from backend
202 # 10102 = invalid arguments
203```
204
205## Architecture
206
207```
208User's Phone (LINE app)
209 ↕ (scan QR / enter PIN)
210LINE Servers (line-chrome-gw.line-apps.com)
211 ↕ (JSON REST + X-Hmac signing)
212LineChromeClient (this repo)
213 ↕ (WASM HMAC via Node.js signer)
214lstm.wasm + lstmSandbox.js
215```
216
217The Chrome Gateway translates JSON ↔ Thrift internally. We never deal with Thrift binary — everything is clean JSON.