import hashlib import sys from base64 import urlsafe_b64encode from secrets import token_urlsafe from typing import Any, Callable, Optional, TypeVar from urllib.parse import parse_qs, urlencode import requests from loguru import logger as _logger from PyQt6.QtCore import QUrl from PyQt6.QtNetwork import QNetworkCookie from PyQt6.QtWebEngineCore import ( QWebEngineUrlRequestInfo, QWebEngineUrlRequestInterceptor, ) from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWidgets import ( QApplication, QHBoxLayout, QMainWindow, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, ) USER_AGENT = "PixivAndroidApp/5.0.234 (Android 11; Pixel 5)" REDIRECT_URI = "https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback" LOGIN_URL = "https://app-api.pixiv.net/web/v1/login" AUTH_TOKEN_URL = "https://oauth.secure.pixiv.net/auth/token" CLIENT_ID = "MOBrBDS8blbauoSck0ZfDbtuzpyT" CLIENT_SECRET = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj" app = QApplication(sys.argv) logger = _logger.opt(colors=True) class RequestInterceptor(QWebEngineUrlRequestInterceptor): code_listener: Optional[Callable[[str], None]] = None def __init__(self): super().__init__() def interceptRequest(self, info: QWebEngineUrlRequestInfo) -> None: method = info.requestMethod().data().decode() url = info.requestUrl().url() if ( self.code_listener and "app-api.pixiv.net" in info.requestUrl().host() and info.requestUrl().path().endswith("callback") ): query = parse_qs(info.requestUrl().query()) code, *_ = query["code"] self.code_listener(code) logger.debug(f"{method} {url}") class WebView(QWebEngineView): def __init__(self): super().__init__() self.cookies: dict[str, str] = {} page = self.page() assert page is not None profile = page.profile() assert profile is not None profile.setHttpUserAgent(USER_AGENT) page.contentsSize().setHeight(768) page.contentsSize().setWidth(432) self.interceptor = RequestInterceptor() profile.setUrlRequestInterceptor(self.interceptor) cookie_store = profile.cookieStore() assert cookie_store is not None cookie_store.cookieAdded.connect(self._on_cookie_added) self.setFixedHeight(896) self.setFixedWidth(414) self.start("about:blank") def start(self, goto: str): self.page().profile().cookieStore().deleteAllCookies() # type: ignore self.cookies.clear() self.load(QUrl(goto)) def _on_cookie_added(self, cookie: QNetworkCookie): domain = cookie.domain() name = cookie.name().data().decode() value = cookie.value().data().decode() self.cookies[name] = value logger.debug(f"Set-Cookie {domain} {name} -> {value!r}") class ResponseDataWidget(QWidget): def __init__(self, webview: WebView): super().__init__() self.webview = webview layout = QVBoxLayout() self.cookie_paste = QPlainTextEdit() self.cookie_paste.setDisabled(True) self.cookie_paste.setPlaceholderText("得到的登录数据将会展示在这里") layout.addWidget(self.cookie_paste) copy_button = QPushButton() copy_button.clicked.connect(self._on_clipboard_copy) copy_button.setText("复制上述登录数据到剪贴板") layout.addWidget(copy_button) self.setLayout(layout) def _on_clipboard_copy(self, checked: bool): if paste_string := self.cookie_paste.toPlainText().strip(): app.clipboard().setText(paste_string) # type: ignore _T = TypeVar("_T", bound="LoginPhrase") class LoginPhrase: @staticmethod def s256(data: bytes): return urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode() @classmethod def oauth_pkce(cls) -> tuple[str, str]: code_verifier = token_urlsafe(32) code_challenge = cls.s256(code_verifier.encode()) return code_verifier, code_challenge def __init__(self: _T, url_open_callback: Callable[[str, _T], None]): self.code_verifier, self.code_challenge = self.oauth_pkce() login_params = { "code_challenge": self.code_challenge, "code_challenge_method": "S256", "client": "pixiv-android", } login_url = f"{LOGIN_URL}?{urlencode(login_params)}" url_open_callback(login_url, self) def code_received(self, code: str): response = requests.post( AUTH_TOKEN_URL, data={ "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "code": code, "code_verifier": self.code_verifier, "grant_type": "authorization_code", "include_policy": "true", "redirect_uri": REDIRECT_URI, }, headers={"User-Agent": USER_AGENT}, ) response.raise_for_status() data: dict[str, Any] = response.json() access_token = data["access_token"] refresh_token = data["refresh_token"] expires_in = data.get("expires_in", 0) return_text = "" return_text += f"access_token: {access_token}\n" return_text += f"refresh_token: {refresh_token}\n" return_text += f"expires_in: {expires_in}\n" return return_text class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Pixiv login helper") layout = QHBoxLayout() self.webview = WebView() layout.addWidget(self.webview) self.form = ResponseDataWidget(self.webview) layout.addWidget(self.form) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget) if __name__ == "__main__": window = MainWindow() window.show() def url_open_callback(url: str, login_phrase: LoginPhrase): def code_listener(code: str): response = login_phrase.code_received(code) window.form.cookie_paste.setPlainText(response) window.webview.interceptor.code_listener = code_listener window.webview.start(url) LoginPhrase(url_open_callback) exit(app.exec())