line-ext-msg 1.0.0__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,17 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Generated debug/output files
13
+ line_dom.html
14
+ line_room.html
15
+ rooms.json
16
+ messages_*.json
17
+ media/
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ First stable release: OOP facade, extended schema, filters, search, media.
6
+
7
+ - `Message.media_data`: opt-in data URI (`include_media_data=True`) for MCP/AI image analysis
8
+ - Download image bubbles to `media/` via in-page blob fetch; `Message.media` holds the path
9
+ - CLI shows `[รูปภาพ: path]` / `[สติกเกอร์]` instead of blank lines
10
+
11
+ - Filters: `date_from/date_to`, `time_from/time_to` (`HH:MM`), `sender`, `keyword` in `get_messages`/`save_messages`/CLI
12
+ - `search_all()` keyword search grouped per room, `unread_full()` unread rooms + today's messages
13
+ - `sender_stats()` pure per-sender counts; `apply_filters()` unit-tested without browser
14
+
15
+ - Explicit save: `get_*`/`list_*` never write files; only `save_rooms`/`save_messages` do
16
+ - CLI `--save` flag (default prints to screen only)
17
+ - Removed `export_room` (use `save_messages`)
18
+
19
+ - OOP rewrite: `LineClient` facade owns lifecycle (context manager)
20
+ - Models: `Room{index,id,name,unread,last_preview,last_time}`,
21
+ `Message{id,date,ts,sender,from_me,type,text,read_count}`, `StepResult`
22
+ - Typed errors: `LoginRequired`, `ExtensionMissing`, `RoomNotFound`, ...
23
+ - `Settings` dataclass (env `LINE_EXT_MSG_*`) replaces config constants
24
+ - `open_room` accepts index, data-mid, name substring, or Room
25
+ - `get_messages(room?, limit, date?)`, `unread_digest()`, `export_room()`
26
+ - CLI flags: `--date`, `--unread`; `Steps` supports `quiet`
27
+ - Removed: `browser.py`, `chrome_launcher.py`, `config.py` (now `session.py`, `chrome.py`, `settings.py`)
28
+
29
+ ## 0.1.0
30
+
31
+ - Restructured to `src/line_ext_msg` package with `line-ext-msg` CLI entry point
32
+ - Checklist startup: Chrome debug, CDP attach, extension, render, login
33
+ - Room listing, latest-N message export to JSON (timestamp + sender + text)
34
+ - Library API for MCP reuse: `list_rooms`, `get_latest_messages`
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ITC6910120
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,79 @@
1
+ Metadata-Version: 2.5
2
+ Name: line-ext-msg
3
+ Version: 1.0.0
4
+ Summary: Pull messages from LINE Chrome Extension via Playwright CDP (own account, read-only)
5
+ Project-URL: Homepage, https://github.com/onlygummy/line-ext-msg
6
+ Author: ITC6910120
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: cdp,chrome-extension,line,playwright
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: playwright>=1.40
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # line-ext-msg
20
+
21
+ ดึงข้อความ (เวลา + คนส่ง + ข้อความ + วันที่ + สถานะ) จาก LINE Chrome Extension ผ่าน Playwright แบบเกาะ Chrome ตัวจริง บัญชีของตัวเอง อ่านอย่างเดียว
22
+
23
+ ## ติดตั้ง
24
+
25
+ ```powershell
26
+ pip install line-ext-msg
27
+ ```
28
+
29
+ ต้องมี Google Chrome ในเครื่อง ไม่ต้องรัน `playwright install` เพราะโปรแกรมเกาะ Chrome ที่มีอยู่ผ่าน CDP ไม่ได้ใช้ bundled chromium
30
+
31
+ ## ใช้งาน CLI
32
+
33
+ ```powershell
34
+ line-ext-msg # เลือกห้องใน Terminal → พิมพ์จออย่างเดียว ไม่สร้างไฟล์
35
+ line-ext-msg --save # + เขียน rooms.json + messages_{index}.json
36
+ line-ext-msg --unread # แสดงเฉพาะห้องที่ไม่อ่าน
37
+ line-ext-msg --limit 10 --date 2026-09-12
38
+ line-ext-msg --date-from 2026-09-01 --date-to 2026-09-12 --keyword "ใบแจ้งหนี้"
39
+ line-ext-msg --search "ใบแจ้งหนี้" # ค้นทุกห้อง สรุปห้องที่เจอ
40
+ ```
41
+
42
+ ครั้งแรก: โปรแกรมเปิด Chrome โปรไฟล์แยก `%LOCALAPPDATA%\line-chrome-debug` ให้เอง ติดตั้ง LINE + ล็อกอินครั้งเดียวในหน้าต่างนั้น หลังจากนั้น auto หมด (Chrome 136+ บล็อก `--remote-debugging-port` บนโปรไฟล์หลักโดยตรง จึงต้องใช้โปรไฟล์แยก)
43
+
44
+ ## หน้าตาไฟล์ JSON ที่ได้
45
+
46
+ ```json
47
+ {
48
+ "room": { "index": 0, "id": "CXX...", "name": "ครอบครัว", "unread": 3,
49
+ "last_preview": "กินข้าวยัง", "last_time": "8:13 AM" },
50
+ "fetched_at": "2026-09-12T01:00:00+00:00",
51
+ "messages": [
52
+ { "id": "0178...", "date": "2026-09-12", "ts": "2026-09-12T08:13:00",
53
+ "sender": "แม่", "from_me": false, "type": "text",
54
+ "text": "กินข้าวยัง", "read_count": null }
55
+ ]
56
+ }
57
+ ```
58
+
59
+ ## ข้อจำกัดที่ควรรู้
60
+
61
+ - อ่านได้เฉพาะแถวที่ render บนจอ (virtualized list) ประวัติย้อนหลังลึกต้อง scroll เพิ่ม (ยังไม่ทำ)
62
+ - ได้เฉพาะที่ UI แสดง ไม่มี API สถานะอ่านรายข้อความ
63
+ - `from_me` เป็น heuristic (ไม่มี username = เราส่ง) ยังไม่ยืนยันด้วย dump ที่มีข้อความของตัวเอง
64
+
65
+ ## dev
66
+
67
+ ```powershell
68
+ uv sync
69
+ uv run pytest
70
+ uv build
71
+ ```
72
+
73
+ ## ถ้าอ่านห้องไม่ได้ (LINE อัปเดต UI)
74
+
75
+ ```powershell
76
+ line-ext-msg --dump # DOM หน้า chats → line_dom.html
77
+ line-ext-msg --dump-room 0 # DOM ในห้อง → line_room.html
78
+ # ส่งไฟล์มาเพื่อจูน selector ใน src/line_ext_msg/settings.py
79
+ ```
@@ -0,0 +1,61 @@
1
+ # line-ext-msg
2
+
3
+ ดึงข้อความ (เวลา + คนส่ง + ข้อความ + วันที่ + สถานะ) จาก LINE Chrome Extension ผ่าน Playwright แบบเกาะ Chrome ตัวจริง บัญชีของตัวเอง อ่านอย่างเดียว
4
+
5
+ ## ติดตั้ง
6
+
7
+ ```powershell
8
+ pip install line-ext-msg
9
+ ```
10
+
11
+ ต้องมี Google Chrome ในเครื่อง ไม่ต้องรัน `playwright install` เพราะโปรแกรมเกาะ Chrome ที่มีอยู่ผ่าน CDP ไม่ได้ใช้ bundled chromium
12
+
13
+ ## ใช้งาน CLI
14
+
15
+ ```powershell
16
+ line-ext-msg # เลือกห้องใน Terminal → พิมพ์จออย่างเดียว ไม่สร้างไฟล์
17
+ line-ext-msg --save # + เขียน rooms.json + messages_{index}.json
18
+ line-ext-msg --unread # แสดงเฉพาะห้องที่ไม่อ่าน
19
+ line-ext-msg --limit 10 --date 2026-09-12
20
+ line-ext-msg --date-from 2026-09-01 --date-to 2026-09-12 --keyword "ใบแจ้งหนี้"
21
+ line-ext-msg --search "ใบแจ้งหนี้" # ค้นทุกห้อง สรุปห้องที่เจอ
22
+ ```
23
+
24
+ ครั้งแรก: โปรแกรมเปิด Chrome โปรไฟล์แยก `%LOCALAPPDATA%\line-chrome-debug` ให้เอง ติดตั้ง LINE + ล็อกอินครั้งเดียวในหน้าต่างนั้น หลังจากนั้น auto หมด (Chrome 136+ บล็อก `--remote-debugging-port` บนโปรไฟล์หลักโดยตรง จึงต้องใช้โปรไฟล์แยก)
25
+
26
+ ## หน้าตาไฟล์ JSON ที่ได้
27
+
28
+ ```json
29
+ {
30
+ "room": { "index": 0, "id": "CXX...", "name": "ครอบครัว", "unread": 3,
31
+ "last_preview": "กินข้าวยัง", "last_time": "8:13 AM" },
32
+ "fetched_at": "2026-09-12T01:00:00+00:00",
33
+ "messages": [
34
+ { "id": "0178...", "date": "2026-09-12", "ts": "2026-09-12T08:13:00",
35
+ "sender": "แม่", "from_me": false, "type": "text",
36
+ "text": "กินข้าวยัง", "read_count": null }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ ## ข้อจำกัดที่ควรรู้
42
+
43
+ - อ่านได้เฉพาะแถวที่ render บนจอ (virtualized list) ประวัติย้อนหลังลึกต้อง scroll เพิ่ม (ยังไม่ทำ)
44
+ - ได้เฉพาะที่ UI แสดง ไม่มี API สถานะอ่านรายข้อความ
45
+ - `from_me` เป็น heuristic (ไม่มี username = เราส่ง) ยังไม่ยืนยันด้วย dump ที่มีข้อความของตัวเอง
46
+
47
+ ## dev
48
+
49
+ ```powershell
50
+ uv sync
51
+ uv run pytest
52
+ uv build
53
+ ```
54
+
55
+ ## ถ้าอ่านห้องไม่ได้ (LINE อัปเดต UI)
56
+
57
+ ```powershell
58
+ line-ext-msg --dump # DOM หน้า chats → line_dom.html
59
+ line-ext-msg --dump-room 0 # DOM ในห้อง → line_room.html
60
+ # ส่งไฟล์มาเพื่อจูน selector ใน src/line_ext_msg/settings.py
61
+ ```
@@ -0,0 +1,20 @@
1
+ """01 - List all LINE chat rooms.
2
+
3
+ Prerequisites: Chrome + LINE Extension installed + logged in
4
+ (profile %LOCALAPPDATA%\\line-chrome-debug).
5
+
6
+ Run: uv run python examples/01_list_rooms.py
7
+ """
8
+ from line_ext_msg import LineClient
9
+
10
+
11
+ def main() -> None:
12
+ with LineClient() as line:
13
+ line.status()
14
+ for room in line.list_rooms():
15
+ unread = f" ({room.unread} ไม่อ่าน)" if room.unread else ""
16
+ print(f"[{room.index}] {room.name}{unread}")
17
+
18
+
19
+ if __name__ == "__main__":
20
+ main()
@@ -0,0 +1,23 @@
1
+ """02 - Export latest messages of one room to JSON.
2
+
3
+ Run: uv run python examples/02_latest_messages.py --room "ครอบครัว" --limit 5
4
+ """
5
+ import argparse
6
+
7
+ from line_ext_msg import LineClient
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser()
12
+ parser.add_argument("--room", required=True, help="Room name substring")
13
+ parser.add_argument("--limit", type=int, default=5)
14
+ args = parser.parse_args()
15
+
16
+ with LineClient() as line:
17
+ line.status()
18
+ out = line.save_messages(args.room, limit=args.limit)
19
+ print(f"บันทึก {out}")
20
+
21
+
22
+ if __name__ == "__main__":
23
+ main()
@@ -0,0 +1,40 @@
1
+ """03 - MCP-style function: no CLI, no input(), returns dict.
2
+
3
+ This is the shape an MCP server (separate repo) can import directly:
4
+ from line_ext_msg import LineClient
5
+
6
+ Run: uv run python examples/03_mcp_style.py
7
+ """
8
+ from dataclasses import asdict
9
+
10
+ from line_ext_msg import LineClient
11
+ from line_ext_msg.errors import LineError
12
+
13
+
14
+ def get_messages(room_name: str, limit: int = 5) -> dict:
15
+ """Return latest messages of the first room matching room_name."""
16
+ try:
17
+ with LineClient(quiet=True) as line:
18
+ line.status()
19
+ room = line.open_room(room_name)
20
+ msgs = line.get_messages(limit=limit)
21
+ return {"ok": True, "room": asdict(room),
22
+ "messages": [asdict(m) for m in msgs]}
23
+ except LineError as e:
24
+ return {"ok": False, "error": f"{type(e).__name__}: {e}"}
25
+
26
+
27
+ def unread_digest() -> dict:
28
+ """Rooms with unread messages plus latest preview."""
29
+ try:
30
+ with LineClient(quiet=True) as line:
31
+ line.status()
32
+ return {"ok": True, "rooms": line.unread_digest()}
33
+ except LineError as e:
34
+ return {"ok": False, "error": f"{type(e).__name__}: {e}"}
35
+
36
+
37
+ if __name__ == "__main__":
38
+ import json
39
+
40
+ print(json.dumps(get_messages("ครอบครัว"), ensure_ascii=False, indent=2))
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "line-ext-msg"
3
+ version = "1.0.0"
4
+ description = "Pull messages from LINE Chrome Extension via Playwright CDP (own account, read-only)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "ITC6910120" }]
9
+ keywords = ["line", "chrome-extension", "playwright", "cdp"]
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+ dependencies = ["playwright>=1.40"]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=8"]
19
+
20
+ [project.scripts]
21
+ line-ext-msg = "line_ext_msg.cli:main"
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/onlygummy/line-ext-msg"
25
+
26
+ [build-system]
27
+ requires = ["hatchling"]
28
+ build-backend = "hatchling.build"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/line_ext_msg"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
@@ -0,0 +1,34 @@
1
+ """line-ext-msg: pull messages from LINE Chrome Extension via Playwright CDP."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from .client import LineClient
6
+ from .messages import sender_stats
7
+ from .errors import (
8
+ AppNotReady,
9
+ AttachFailed,
10
+ ChromeNotReady,
11
+ ExtensionMissing,
12
+ LineError,
13
+ LoginRequired,
14
+ RoomNotFound,
15
+ )
16
+ from .models import Message, Room, StepResult
17
+ from .settings import Settings
18
+
19
+ __all__ = [
20
+ "__version__",
21
+ "LineClient",
22
+ "sender_stats",
23
+ "Room",
24
+ "Message",
25
+ "StepResult",
26
+ "Settings",
27
+ "LineError",
28
+ "ChromeNotReady",
29
+ "AttachFailed",
30
+ "ExtensionMissing",
31
+ "AppNotReady",
32
+ "LoginRequired",
33
+ "RoomNotFound",
34
+ ]
@@ -0,0 +1,58 @@
1
+ """Login check: distinguish 'not logged in' from 'selector mismatch'."""
2
+
3
+ from playwright.sync_api import Page
4
+
5
+ from .settings import SELECTORS
6
+
7
+ # Fast login-screen locators (CSS-module hashes change, so match stable parts).
8
+ LOGIN_LOCATORS = [
9
+ "[class*='loginPage']",
10
+ "text=QR code login",
11
+ "text=Log in",
12
+ ]
13
+
14
+ # Fallback text markers when locators miss (checked on small scope only).
15
+ LOGIN_MARKERS = [
16
+ "qr code", "qr login", "email address",
17
+ "เข้าสู่ระบบ", "ล็อกอิน",
18
+ ]
19
+
20
+
21
+ def _has_login_ui(page: Page) -> bool:
22
+ """True if any login marker is visible (cheap locator checks)."""
23
+ for sel in LOGIN_LOCATORS:
24
+ try:
25
+ if page.locator(sel).count() > 0:
26
+ return True
27
+ except Exception:
28
+ continue
29
+ return False
30
+
31
+
32
+ def check_login(page: Page, timeout_ms: int = 10000) -> tuple[bool, str]:
33
+ """Poll chat UI vs login UI; return as soon as either appears.
34
+
35
+ Return (logged_in, reason): 'chat' / 'login' / 'unknown'.
36
+ Caller must ensure the app finished rendering (wait_for_app_ready).
37
+ """
38
+ # Poll room rows (not the container: it may not exist as one element).
39
+ rows = [s.strip() for s in SELECTORS["room_item"].split(",")]
40
+ # Poll every 500ms; worst case only when page genuinely stuck.
41
+ for _ in range(max(1, timeout_ms // 500)):
42
+ for sel in rows:
43
+ try:
44
+ if page.locator(sel).count() > 0:
45
+ return True, "chat"
46
+ except Exception:
47
+ continue
48
+ if _has_login_ui(page):
49
+ return False, "login"
50
+ page.wait_for_timeout(500)
51
+ # Final text fallback on rendered body before giving up.
52
+ try:
53
+ text = (page.inner_text("body") or "").lower()
54
+ except Exception:
55
+ text = ""
56
+ if any(m in text for m in LOGIN_MARKERS):
57
+ return False, "login"
58
+ return False, "unknown"
@@ -0,0 +1,94 @@
1
+ """Chrome lifecycle: ensure a debuggable instance on the isolated profile."""
2
+
3
+ import os
4
+ import subprocess
5
+ import time
6
+ import urllib.request
7
+
8
+ from .settings import Settings
9
+
10
+
11
+ def expand(path: str) -> str:
12
+ """Expand %VAR% on Windows and ~ on all platforms."""
13
+ return os.path.expandvars(os.path.expanduser(path))
14
+
15
+
16
+ def find_chrome_exe() -> str | None:
17
+ """Search common install locations, return exe path or None."""
18
+ candidates = [
19
+ r"C:\Program Files\Google\Chrome\Application\chrome.exe",
20
+ r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
21
+ expand(r"%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe"),
22
+ ]
23
+ for path in candidates:
24
+ if path and os.path.isfile(path):
25
+ return path
26
+ return None
27
+
28
+
29
+ def is_debug_ready(settings: Settings, timeout_sec: int = 2) -> bool:
30
+ """Check CDP endpoint responds with valid Browser field."""
31
+ try:
32
+ with urllib.request.urlopen(f"{settings.cdp_endpoint}/json/version", timeout=timeout_sec) as res:
33
+ return "Browser" in res.read().decode("utf-8", errors="ignore")
34
+ except Exception:
35
+ return False
36
+
37
+
38
+ def is_headless(settings: Settings, timeout_sec: int = 2) -> bool:
39
+ """Detect a headless instance squatting the debug port (no visible window)."""
40
+ try:
41
+ with urllib.request.urlopen(f"{settings.cdp_endpoint}/json/version", timeout=timeout_sec) as res:
42
+ return "Headless" in res.read().decode("utf-8", errors="ignore")
43
+ except Exception:
44
+ return False
45
+
46
+
47
+ def _is_profile_locked(data_dir: str) -> bool:
48
+ """Detect SingletonLock (profile in use by a normal window)."""
49
+ return any(
50
+ os.path.exists(os.path.join(data_dir, name))
51
+ for name in ("SingletonLock", "SingletonSocket", "SingletonCookie")
52
+ )
53
+
54
+
55
+ def start_chrome_debug(settings: Settings) -> None:
56
+ """Start detached Chrome on the isolated profile.
57
+
58
+ Chrome 136+ ignores --remote-debugging-port on the default profile,
59
+ hence the isolated dir. Login + install persist there after first setup.
60
+ """
61
+ from .errors import ChromeNotReady
62
+
63
+ exe = find_chrome_exe()
64
+ if exe is None:
65
+ raise ChromeNotReady("ไม่พบ chrome.exe กรุณาติดตั้ง Chrome ก่อน")
66
+ data_dir = expand(settings.profile_dir)
67
+ if _is_profile_locked(data_dir):
68
+ raise ChromeNotReady("โปรไฟล์ Chrome ถูกใช้อยู่ ปิดหน้าต่าง debug เก่าก่อนแล้วรันใหม่")
69
+ os.makedirs(data_dir, exist_ok=True)
70
+ subprocess.Popen(
71
+ [exe, f"--remote-debugging-port={settings.port}", f"--user-data-dir={data_dir}"],
72
+ stdout=subprocess.DEVNULL,
73
+ stderr=subprocess.DEVNULL,
74
+ creationflags=getattr(subprocess, "DETACHED_PROCESS", 0),
75
+ )
76
+ for _ in range(30):
77
+ if is_debug_ready(settings, timeout_sec=1):
78
+ return
79
+ time.sleep(0.5)
80
+ raise ChromeNotReady("เปิด Chrome แล้วแต่พอร์ต debug ไม่ตอบใน 15 วิ")
81
+
82
+
83
+ def ensure_chrome(settings: Settings) -> None:
84
+ """Ensure headed debug Chrome is running; start it if the port is closed."""
85
+ from .errors import ChromeNotReady
86
+
87
+ if is_debug_ready(settings):
88
+ if is_headless(settings):
89
+ raise ChromeNotReady(
90
+ "พอร์ต debug ถูกโปรแกรมอื่น (headless) ใช้อยู่ ไม่มีหน้าต่างให้เห็น "
91
+ "รัน: Get-Process chrome | Stop-Process -Force แล้วรันใหม่"
92
+ )
93
+ return
94
+ start_chrome_debug(settings)
@@ -0,0 +1,99 @@
1
+ """Thin CLI over LineClient: parse args, print results."""
2
+ import argparse
3
+
4
+ from .client import LineClient
5
+ from .errors import LineError
6
+ from .models import Room
7
+
8
+
9
+ def _display(m) -> str:
10
+ """Readable line: text, or media label when the bubble has no text."""
11
+ if m.text:
12
+ return m.text[:80]
13
+ if m.type == "image":
14
+ return f"[รูปภาพ: {m.media or 'โหลดไม่สำเร็จ'}]"
15
+ if m.type == "sticker":
16
+ return "[สติกเกอร์]"
17
+ if m.type == "system":
18
+ return m.text
19
+ return f"[{m.type}]"
20
+
21
+
22
+ def pick_room(rooms: list[Room]) -> Room:
23
+ """Show rooms in terminal and return user choice."""
24
+ print("\nห้องแชททั้งหมด:")
25
+ for r in rooms:
26
+ unread = f" ({r.unread} ไม่อ่าน)" if r.unread else ""
27
+ print(f" [{r.index}] {r.name}{unread}")
28
+ while True:
29
+ raw = input("พิมพ์เลขห้อง > ").strip()
30
+ if raw.isdigit() and any(r.index == int(raw) for r in rooms):
31
+ return next(r for r in rooms if r.index == int(raw))
32
+ print("เลขไม่ถูกต้อง ลองใหม่")
33
+
34
+
35
+ def main():
36
+ parser = argparse.ArgumentParser(description="ดึงข้อความล่าสุดจาก LINE Extension")
37
+ parser.add_argument("--limit", type=int, default=5, help="จำนวนข้อความล่าสุด")
38
+ parser.add_argument("--date", default=None, help="กรองเฉพาะวันที่ YYYY-MM-DD")
39
+ parser.add_argument("--date-from", default=None, help="ตั้งแต่วันที่ YYYY-MM-DD")
40
+ parser.add_argument("--date-to", default=None, help="ถึงวันที่ YYYY-MM-DD")
41
+ parser.add_argument("--time-from", default=None, help="ตั้งแต่เวลา HH:MM")
42
+ parser.add_argument("--time-to", default=None, help="ถึงเวลา HH:MM")
43
+ parser.add_argument("--sender", default=None, help="กรองชื่อคนส่ง (บางส่วน)")
44
+ parser.add_argument("--keyword", default=None, help="ค้นคำในข้อความ")
45
+ parser.add_argument("--search", default=None, metavar="KEYWORD",
46
+ help="ค้นทุกห้อง แล้วสรุปห้องที่เจอ")
47
+ parser.add_argument("--unread", action="store_true", help="แสดงเฉพาะห้องที่ไม่อ่าน")
48
+ parser.add_argument("--save", action="store_true", help="เขียน rooms.json + messages ลงไฟล์ (default พิมพ์จออย่างเดียว)")
49
+ parser.add_argument("--dump", action="store_true", help="บันทึก DOM ดิบเพื่อจูน selector")
50
+ parser.add_argument("--dump-room", type=int, default=None, metavar="INDEX",
51
+ help="เปิดห้องลำดับ INDEX แล้วบันทึก DOM เพื่อจูน selector ข้อความ")
52
+ args = parser.parse_args()
53
+
54
+ try:
55
+ with LineClient() as line:
56
+ if args.dump:
57
+ state = line.dump_page()
58
+ print(f"บันทึก line_dom.html แล้ว (state={state}) ส่งไฟล์นี้มาเพื่อจูน selector")
59
+ return
60
+ line.status()
61
+ if args.dump_room is not None:
62
+ room = line.dump_room(args.dump_room)
63
+ print(f"บันทึก line_room.html แล้ว (ห้อง {room.name}) ส่งไฟล์นี้มาเพื่อจูน selector ข้อความ")
64
+ return
65
+ rooms = line.list_rooms(unread_only=args.unread)
66
+ if not rooms:
67
+ print("อ่านรายชื่อห้องไม่ได้ รัน --dump แล้วส่ง line_dom.html มา")
68
+ return
69
+ if args.save:
70
+ path = line.save_rooms(unread_only=args.unread)
71
+ print(f"บันทึก {path} ({len(rooms)} ห้อง)")
72
+ if args.search:
73
+ for hit in line.search_all(args.search, date_from=args.date_from,
74
+ date_to=args.date_to or args.date):
75
+ print(f"\n== {hit['room']['name']} ({len(hit['messages'])} ข้อความ) ==")
76
+ for m in hit["messages"]:
77
+ print(f"[{m['date']} {m['ts']}] {m['sender']}: {m['text'][:80]}")
78
+ return
79
+ chosen = pick_room(rooms)
80
+ filt = dict(limit=args.limit, date=args.date, date_from=args.date_from,
81
+ date_to=args.date_to, time_from=args.time_from, time_to=args.time_to,
82
+ sender=args.sender, keyword=args.keyword, media_dir="media")
83
+ if args.save:
84
+ out = line.save_messages(chosen, **filt)
85
+ print(f"บันทึก {out}")
86
+ print(f"กำลังเปิดห้อง {chosen.name} ...", flush=True)
87
+ msgs = line.get_messages(chosen, **filt)
88
+ if not msgs:
89
+ print("เปิดห้องได้แต่อ่านข้อความไม่ได้")
90
+ print(f"รัน uv run line-ext-msg --dump-room {chosen.index} แล้วส่ง line_room.html มา")
91
+ return
92
+ for m in msgs:
93
+ print(f"[{m.date} {m.ts}] {m.sender}: {_display(m)}")
94
+ except LineError as e:
95
+ print(e)
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()