Spaces:
Paused
Paused
Download channel_auth.py from izuemon/any-env-code: direct link, hf CLI and curl.
- Browser
- Download file 13.7 kB
-
https://huggingface.co/spaces/izuemon/any-env-code/resolve/main/channel_auth.py
- Command line
-
hf download hf://spaces/izuemon/any-env-code/channel_auth.py
-
curl -L -o channel_auth.py https://huggingface.co/spaces/izuemon/any-env-code/resolve/main/channel_auth.py
13.7 kB
| """ | |
| ChannelWorks クライアントライブラリ | |
| 使い方: | |
| from channel_works import ChannelWorksClient | |
| client = ChannelWorksClient( | |
| email="you@example.com", | |
| password="your-password", | |
| log_level=ChannelWorksClient.LOG_DEBUG, | |
| ) | |
| client.login() # sign_in + boot | |
| client.start_auto_touch() # 10分ごとに自動で touch を叩いてトークン更新 | |
| tokens = client.get_tokens() # 最新トークンを取得 | |
| ... | |
| client.stop_auto_touch() | |
| log_level: | |
| 0 = 何も出力しない | |
| 1 = エラー・警告のみ (デフォルト) | |
| 2 = デバッグログも出力 | |
| """ | |
| import json | |
| import threading | |
| from collections import OrderedDict | |
| from urllib.parse import urlencode | |
| import requests | |
| SIGN_IN_URI = "https://api.channel.works/desk/account/sign-in" | |
| BOOT_URI = "https://api.channel.io/front/v8/elastic/plugins/b823a684-f97e-4e4e-bf12-48b70c1939ae/boot" | |
| TOUCH_URI = "https://api.channel.works/desk/account/touch" | |
| BOOT_URL_TEMPLATE = "https://channel.works/{room_id}/user-chats" | |
| BOOT_AVATAR_URL = "https://cf.channel.io/avatar/emoji/bee.f7dc8e.png" | |
| DEFAULT_TOUCH_INTERVAL = 600 # 10 分 | |
| # Set-Cookie から抜き出す対象トークン | |
| _TOKEN_NAMES = ("x-account", "x-account-refresh", "ch-session-1", "ch-veil-id") | |
| class ChannelWorksClient: | |
| # ログレベル | |
| LOG_NONE = 0 | |
| LOG_ERROR = 1 | |
| LOG_DEBUG = 2 | |
| def __init__( | |
| self, | |
| room_id, | |
| email=None, | |
| password=None, | |
| log_level=LOG_ERROR, | |
| touch_interval=DEFAULT_TOUCH_INTERVAL, | |
| timeout=30, | |
| ): | |
| if not room_id: | |
| raise ValueError("room_id は必須です。") | |
| self.room_id = room_id | |
| self.email = email | |
| self.password = password | |
| self.log_level = log_level | |
| self.touch_interval = touch_interval | |
| self.timeout = timeout | |
| self._lock = threading.RLock() | |
| # 最新トークン(この値は常に上書きされる。固定値にはならない) | |
| self._x_account = None | |
| self._x_account_refresh = None | |
| self._ch_session_1 = None | |
| self._ch_veil_id = None | |
| # boot に必要なアカウント情報 | |
| self._account_name = None | |
| self._account_email = None | |
| self._account_id = None | |
| self._member_hash = None | |
| # 自動 touch 用スレッド | |
| self._touch_thread = None | |
| self._touch_stop = threading.Event() | |
| # ------------------------------------------------------------------ | |
| # ログ | |
| # ------------------------------------------------------------------ | |
| def _log_debug(self, msg): | |
| if self.log_level >= self.LOG_DEBUG: | |
| print(f"[DEBUG] {msg}") | |
| def _log_error(self, msg): | |
| if self.log_level >= self.LOG_ERROR: | |
| print(f"[ERROR] {msg}") | |
| def set_log_level(self, level): | |
| self.log_level = level | |
| # ------------------------------------------------------------------ | |
| # 内部ユーティリティ | |
| # ------------------------------------------------------------------ | |
| def _common_headers(): | |
| return { | |
| "accept": "application/json", | |
| "accept-language": "ja", | |
| "content-type": "application/json", | |
| "priority": "u=1, i", | |
| "sec-ch-ua": "\"Chromium\";v=\"152\", \"Not?A_Brand\";v=\"24\", \"Google Chrome\";v=\"152\"", | |
| "sec-ch-ua-mobile": "?0", | |
| "sec-ch-ua-platform": "\"Windows\"", | |
| "sec-fetch-dest": "empty", | |
| "sec-fetch-mode": "cors", | |
| "sec-fetch-site": "same-site", | |
| "Referer": "https://channel.works/", | |
| } | |
| def _get_set_cookie_headers(resp): | |
| try: | |
| headers = resp.raw.headers.getlist("Set-Cookie") | |
| if headers: | |
| return headers | |
| except Exception: | |
| pass | |
| raw = resp.headers.get("Set-Cookie") | |
| return [raw] if raw else [] | |
| def _extract_new_tokens(set_cookie_headers): | |
| new_tokens = {} | |
| for set_cookie in set_cookie_headers: | |
| first_part = set_cookie.split(";", 1)[0].strip() | |
| if "=" not in first_part: | |
| continue | |
| name, value = first_part.split("=", 1) | |
| name = name.strip() | |
| if name in _TOKEN_NAMES and value: | |
| new_tokens[name] = value | |
| return new_tokens | |
| def _build_cookie_header(self): | |
| cookies = OrderedDict() | |
| if self._ch_session_1: | |
| cookies["ch-session-1"] = self._ch_session_1 | |
| if self._ch_veil_id: | |
| cookies["ch-veil-id"] = self._ch_veil_id | |
| if self._x_account: | |
| cookies["x-account"] = self._x_account | |
| if self._x_account_refresh: | |
| cookies["x-account-refresh"] = self._x_account_refresh | |
| cookies["x-account-refresh-bound"] = "false" | |
| return "; ".join(f"{k}={v}" for k, v in cookies.items()) | |
| def _apply_new_tokens(self, new_tokens): | |
| """Set-Cookie から取得した新トークンを保持値に反映する。""" | |
| with self._lock: | |
| if "x-account" in new_tokens: | |
| self._x_account = new_tokens["x-account"] | |
| if "x-account-refresh" in new_tokens: | |
| self._x_account_refresh = new_tokens["x-account-refresh"] | |
| if "ch-session-1" in new_tokens: | |
| self._ch_session_1 = new_tokens["ch-session-1"] | |
| if "ch-veil-id" in new_tokens: | |
| self._ch_veil_id = new_tokens["ch-veil-id"] | |
| # ------------------------------------------------------------------ | |
| # 認証 | |
| # ------------------------------------------------------------------ | |
| def sign_in(self): | |
| if not self.email or not self.password: | |
| raise ValueError("email と password を指定してください。") | |
| headers = self._common_headers() | |
| headers["x-supports-refresh"] = "true" | |
| body = json.dumps({"email": self.email, "password": self.password}).encode("utf-8") | |
| self._log_debug(f"sign_in POST {SIGN_IN_URI}") | |
| resp = requests.post(SIGN_IN_URI, headers=headers, data=body, timeout=self.timeout) | |
| if not (200 <= resp.status_code < 300): | |
| self._log_error(f"sign_in 失敗 status={resp.status_code} body={resp.text}") | |
| return resp | |
| set_cookies = self._get_set_cookie_headers(resp) | |
| new_tokens = self._extract_new_tokens(set_cookies) | |
| self._apply_new_tokens(new_tokens) | |
| self._log_debug(f"sign_in 取得トークン: {sorted(new_tokens.keys())}") | |
| if not self._x_account or not self._x_account_refresh: | |
| self._log_error("sign_in 応答から x-account / x-account-refresh を取得できませんでした。") | |
| try: | |
| data = resp.json() | |
| except ValueError: | |
| self._log_error("sign_in 応答の JSON パースに失敗しました。") | |
| return resp | |
| account_info = data["account"] | |
| with self._lock: | |
| self._account_name = account_info["name"] | |
| self._account_email = account_info["email"] | |
| self._account_id = account_info["id"] | |
| self._member_hash = data["memberHash"] | |
| self._log_debug("sign_in 成功") | |
| return resp | |
| def boot(self): | |
| if not self._account_id or not self._member_hash: | |
| self._log_error("boot 前に sign_in を実行してください。") | |
| return None | |
| profile = json.dumps( | |
| { | |
| "name": self._account_name, | |
| "email": self._account_email, | |
| "avatarUrl": BOOT_AVATAR_URL, | |
| }, | |
| ensure_ascii=False, | |
| separators=(",", ":"), | |
| ) | |
| boot_url_value = BOOT_URL_TEMPLATE.format(room_id=self.room_id) | |
| body = urlencode({ | |
| "url": boot_url_value, # ← 変更 | |
| "profile": profile, | |
| "memberId": self._account_id, | |
| "memberHash": self._member_hash, | |
| }).encode("utf-8") | |
| headers = { | |
| "accept": "application/json", | |
| "accept-language": "ja", | |
| "content-type": "application/x-www-form-urlencoded", | |
| "priority": "u=1, i", | |
| "sec-ch-ua": "\"Chromium\";v=\"152\", \"Not?A_Brand\";v=\"24\", \"Google Chrome\";v=\"152\"", | |
| "sec-ch-ua-mobile": "?0", | |
| "sec-ch-ua-platform": "\"Windows\"", | |
| "sec-fetch-dest": "empty", | |
| "sec-fetch-mode": "cors", | |
| "sec-fetch-site": "cross-site", | |
| "sec-fetch-storage-access": "none", | |
| "Referer": "https://channel.works/", | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " # User-Agentは必須 | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/152.0.0.0 Safari/537.36" | |
| ), | |
| } | |
| self._log_debug(f"boot POST {BOOT_URI} memberId={self._account_id}") | |
| resp = requests.post(BOOT_URI, headers=headers, data=body, timeout=self.timeout) | |
| if not (200 <= resp.status_code < 300): | |
| self._log_error(f"boot 失敗 status={resp.status_code} body={resp.text}") | |
| return resp | |
| # boot 応答で ch-session-1 / ch-veil-id が降ってくる場合に備えて反映 | |
| self._apply_new_tokens(self._extract_new_tokens(self._get_set_cookie_headers(resp))) | |
| try: | |
| data = resp.json() | |
| except ValueError: | |
| self._log_error("boot 応答の JSON パースに失敗しました。") | |
| return resp | |
| with self._lock: | |
| self._ch_session_1 = data.get("sessionJWT", self._ch_session_1) | |
| self._ch_veil_id = data.get("veilId", self._ch_veil_id) | |
| self._log_debug("boot 成功") | |
| return resp | |
| def login(self): | |
| """sign_in → boot をまとめて実行する。""" | |
| self.sign_in() | |
| self.boot() | |
| # ------------------------------------------------------------------ | |
| # touch | |
| # ------------------------------------------------------------------ | |
| def touch(self): | |
| """最新トークンを使って touch を 1 回叩き、返ってきた新トークンで保持値を更新する。""" | |
| with self._lock: | |
| if not self._x_account: | |
| self._log_error("touch 実行不可: x-account 未取得") | |
| return None | |
| account = self._x_account | |
| cookie_header = self._build_cookie_header() | |
| headers = self._common_headers() | |
| headers["x-account"] = account | |
| headers["Cookie"] = cookie_header | |
| body = b'{"refreshOnly":true,"skipSessionExtension":false}' | |
| self._log_debug(f"touch POST {TOUCH_URI}") | |
| try: | |
| resp = requests.post(TOUCH_URI, headers=headers, data=body, timeout=self.timeout) | |
| except Exception as e: | |
| self._log_error(f"touch 通信例外: {e!r}") | |
| return None | |
| if not (200 <= resp.status_code < 300): | |
| self._log_error(f"touch 失敗 status={resp.status_code} body={resp.text}") | |
| return resp | |
| new_tokens = self._extract_new_tokens(self._get_set_cookie_headers(resp)) | |
| self._apply_new_tokens(new_tokens) | |
| self._log_debug(f"touch 成功 更新トークン: {sorted(new_tokens.keys()) or '(更新なし)'}") | |
| return resp | |
| # ------------------------------------------------------------------ | |
| # トークン取得 | |
| # ------------------------------------------------------------------ | |
| def get_tokens(self): | |
| """常に最新のトークンを辞書で返す。""" | |
| with self._lock: | |
| return { | |
| "x-account": self._x_account, | |
| "x-account-refresh": self._x_account_refresh, | |
| "ch-session-1": self._ch_session_1, | |
| "ch-veil-id": self._ch_veil_id, | |
| } | |
| # ------------------------------------------------------------------ | |
| # 自動 touch | |
| # ------------------------------------------------------------------ | |
| def start_auto_touch(self): | |
| """バックグラウンドで touch_interval 秒ごとに touch を実行する。""" | |
| with self._lock: | |
| if self._touch_thread and self._touch_thread.is_alive(): | |
| self._log_debug("auto-touch は既に起動中") | |
| return | |
| self._touch_stop.clear() | |
| self._touch_thread = threading.Thread( | |
| target=self._touch_loop, name="ChannelWorksAutoTouch", daemon=True | |
| ) | |
| self._touch_thread.start() | |
| self._log_debug(f"auto-touch 開始 (interval={self.touch_interval}s)") | |
| def stop_auto_touch(self): | |
| """自動 touch を停止する。""" | |
| self._touch_stop.set() | |
| thread = self._touch_thread | |
| if thread and thread.is_alive(): | |
| thread.join(timeout=5) | |
| self._touch_thread = None | |
| self._log_debug("auto-touch 停止") | |
| def _touch_loop(self): | |
| while not self._touch_stop.wait(self.touch_interval): | |
| try: | |
| self.touch() | |
| except Exception as e: | |
| self._log_error(f"auto-touch 例外: {e!r}") | |
| # ------------------------------------------------------------------ | |
| # コンテキストマネージャ | |
| # ------------------------------------------------------------------ | |
| def close(self): | |
| self.stop_auto_touch() | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, exc_type, exc, tb): | |
| self.close() | |