Line Client
by @2manslkh
LINE messaging integration via Chrome extension gateway. Send/read LINE messages, manage contacts, groups, profile, and reactions. Authenticate with QR code...
clawhub install line-apiπ About This Skill
name: line-client description: 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).
LINE Client Skill
Full LINE messaging client via the Chrome extension gateway JSON API.
Repo & Files
/data/workspace/line-client (github.com/2manslkh/line-api)src/chrome_client.py β LineChromeClientsrc/auth/qr_login.py β QRLoginsrc/hmac/signer.js (Node.js, auto-starts on port 18944)~/.line-client/tokens.json~/.line-client/sqr_certlstm.wasm + lstmSandbox.js (required, in repo root)Quick Start
import json
from pathlib import Path
from src.chrome_client import LineChromeClienttokens = 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 qrcodesigner = 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
1.createSession β session ID
2. createQrCode β callback URL (append ?secret={curve25519_pubkey}&e2eeVersion=1)
3. checkQrCodeVerified β poll until scan (uses X-Line-Session-ID, no origin header)
4. verifyCertificate β MUST be called even if it fails (required state transition!)
5. createPinCode β 6-digit PIN (skip if cert verified in step 4)
6. checkPinCodeVerified β poll until user enters PIN
7. qrCodeLoginV2 β JWT token + certificate + refresh tokenServer-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:
path + body β base64 β X-HmacError Handling
from src.chrome_client import APIErrortry:
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.
π‘ Examples
import json
from pathlib import Path
from src.chrome_client import LineChromeClienttokens = 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.