line-client
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).
git clone https://github.com/2manslkh/line-api
git clone --depth=1 https://github.com/2manslkh/line-api ~/.claude/skills/2manslkh-line-api-line-client
SKILL.mdLINE Client Skill
Full LINE messaging client via the Chrome extension gateway JSON API.
Repo & Files
- Repo:
(github.com/2manslkh/line-api)/data/workspace/line-client - Main client:
→src/chrome_client.pyLineChromeClient - QR login:
→src/auth/qr_login.pyQRLogin - HMAC signer:
(Node.js, auto-starts on port 18944)src/hmac/signer.js - Token storage:
~/.line-client/tokens.json - Certificate cache:
~/.line-client/sqr_cert - WASM files:
+lstm.wasm
(required, in repo root)lstmSandbox.js
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"]) # Check if chat has E2EE before sending if client.is_e2ee_chat("C..."): print("⚠️ E2EE chat — send_message won't work, see E2EE section below") else: client.send_message("C...", "Hello!") # Send a message (non-E2EE chats only) client.send_message("U...", "Hello!") # Get profile profile = client.get_profile()
Tokens expire in ~7 days. If expired (
APIError(10051)), re-run QR login.
⚠️ E2EE Limitation
Groups with E2EE v2 enabled cannot receive messages via
.send_message()
- Error: code 82 "types mismatch" when sending to E2EE chats
- Messages in E2EE chats appear with
field (encrypted) instead of plaintextchunkstext - Use
to check before sendingclient.is_e2ee_chat(chat_id) - E2EE encryption module exists in
but is NOT wired intosrc/e2ee/crypto.py
yetsend_message - Reading messages from E2EE chats works (returns encrypted chunks)
- Sending requires encrypting with each member's Curve25519 public key — not yet implemented
Workaround: For E2EE groups, use the LINE Official Account API or have the user send manually.
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( on_qr=lambda url: send_qr_image_to_user(qrcode.make(url)), on_pin=lambda pin: send_pin_to_user_IMMEDIATELY(pin), # TIME SENSITIVE! on_status=lambda 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
→ session IDcreateSession
→ callback URL (appendcreateQrCode
)?secret={curve25519_pubkey}&e2eeVersion=1
— poll until scan (usescheckQrCodeVerified
, noX-Line-Session-ID
header)origin
— MUST be called even if it fails (required state transition!)verifyCertificate
→ 6-digit PIN (skip if cert verified in step 4)createPinCode
— poll until user enters PINcheckPinCodeVerified
→ JWT token + certificate + refresh tokenqrCodeLoginV2
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..."}.
Standalone Login (Recommended for Agents)
Best for automation — writes QR/PIN/status to files for fast pickup:
# 1. Start HMAC signer cd /path/to/line-client && node src/hmac/signer.js serve & # 2. Run login (background) python3 scripts/qr_login_standalone.py & # 3. Watch for QR (send to user via messaging) # File: /data/workspace/line_qr.png # 4. Watch for PIN (relay to user IMMEDIATELY — 60s window!) # File: /data/workspace/line_pin.txt # 5. Check completion # File: /data/workspace/line_done.txt → "OK:<mid>" or "FAILED"
Agent orchestration pattern:
# Start login in background nohup python3 scripts/qr_login_standalone.py > /tmp/line_login.log 2>&1 & # Poll for QR ready while [ ! -f /data/workspace/line_status.txt ] || ! grep -q QR_READY /data/workspace/line_status.txt; do sleep 0.5 done # → Send /data/workspace/line_qr.png to user # Poll for PIN (fast — check every 0.2s) while [ ! -f /data/workspace/line_pin.txt ]; do sleep 0.2 done PIN=$(cat /data/workspace/line_pin.txt) # → Send PIN to user IMMEDIATELY # Poll for completion while [ ! -f /data/workspace/line_done.txt ]; do sleep 1 done # → Check result
Critical learnings:
- Always clear
before login to force PIN prompt~/.line-client/sqr_cert - PIN window is ~60 seconds — relay speed is everything
- The standalone script clears cert automatically
- If running from a subagent, the subagent tool-call latency can eat the PIN window
- Best approach: poll PIN file from main thread with minimal sleep interval
Dependencies
# Debian/Ubuntu (no pip needed) apt-get install -y python3-requests python3-qrcode python3-pil python3-nacl python3-cryptography python3-httpx python3-rsa python3-pycryptodome # Also need Crypto alias if pycryptodome installs as Cryptodome ln -sf /usr/lib/python3/dist-packages/Cryptodome /usr/lib/python3/dist-packages/Crypto
All API Methods
Contacts & Friends
| Method | Args | Description |
|---|---|---|
| — | Get your own profile (displayName, mid, statusMessage, etc.) |
| mid: str | Get a single contact's profile |
| mids: list[str] | Get multiple contacts |
| — | List all friend MIDs |
| userid: str | Search by LINE ID |
| mid: str | Add friend by MID |
| phones: list[str] | Search by phone numbers |
| mid: str | Add friend (RelationService) |
| — | List blocked MIDs |
| — | List blocked recommendations |
| mid: str | Block a contact |
| mid: str | Unblock a contact |
| mid: str | Block a friend suggestion |
| mid, flag: int, value: str | Update contact setting (e.g. mute) |
| — | List favorited contact MIDs |
| — | List friend suggestions |
Messages
| Method | Args | Description |
|---|---|---|
| to: str, text: str, reply_to: str (opt) | Send a text message. Supports replies via |
| message_id: str | Unsend/delete a sent message |
| chat_id: str | Get latest messages in a chat |
| chat_id, end_seq: int | Paginated history (older messages) |
| message_ids: list[str] | Fetch specific messages |
| — | Get chat list with last message (inbox view) |
| chat_ids: list[str] | Get specific chats with last message |
| chat_ids: list[str] | Get read receipt info |
| chat_id, last_message_id: str | Mark messages as read |
| chat_id, last_message_id: str | Remove chat from inbox |
| to, postback_data: str | Send postback (bot interactions) |
Chats & Groups
| Method | Args | Description |
|---|---|---|
| chat_ids: list[str] | Get chat/group details |
| — | List all chat MIDs (groups + invites) |
| name: str, target_mids: list[str] | Create a new group chat |
| chat_id: str | Accept group invite |
| chat_id: str | Reject group invite |
| chat_id: str, mids: list[str] | Invite users to group |
| chat_id: str, mids: list[str] | Cancel pending invites |
| chat_id: str, mids: list[str] | Kick members from group |
| chat_id: str | Leave a group chat |
| chat_id: str, updates: dict | Update group name/settings |
| chat_id: str, hidden: bool | Archive/unarchive a chat |
| room_ids: list[str] | Get legacy room info |
| room_id: str, mids: list[str] | Invite to legacy room |
| room_id: str | Leave legacy room |
Reactions
| Method | Args | Description |
|---|---|---|
| message_id: str, type: int | React to a message. Types: 2=like, 3=love, 4=laugh, 5=surprised, 6=sad, 7=angry |
| message_id: str | Remove your reaction |
Profile & Settings
| Method | Args | Description |
|---|---|---|
| attr: int, value: str | Update profile. Attrs: 2=DISPLAY_NAME, 16=STATUS_MESSAGE, 4=PICTURE_STATUS |
| message: str | Shortcut: update status message |
| name: str | Shortcut: update display name |
| — | Get all account settings |
| attr_bitset: int | Get specific settings |
| attr_bitset: int, settings: dict | Update settings |
Polling & Events
| Method | Args | Description |
|---|---|---|
| — | Get latest operation revision number |
| — | Fetch pending operations (may long-poll) |
| — | Generator yielding operations as they arrive |
| handler: Callable(msg, client) | Start polling thread, calls handler on new messages. Op types: 26=SEND_MESSAGE, 27=RECEIVE_MESSAGE |
| — | Stop the polling thread |
Other Services
| Method | Args | Description |
|---|---|---|
| — | Get LINE server timestamp |
| — | Get server configurations |
| — | Get RSA key for auth |
| channel_id: str | Issue channel token (LINE Login/LIFF) |
| mid: str | Get official account info |
| mid: str | Report a user |
| mid: str | Add friend (RelationService) |
| — | Logout and invalidate token |
MID Format
LINE identifies entities by MID:
orU...
→ User (toType=0)u...
orC...
→ Group chat (toType=2)c...
orR...
→ Room (toType=1)r...
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
→ base64 →path + bodyX-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.