File size: 6,432 Bytes
0a1b571 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
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"<y>{method}</y> <u>{url}</u>")
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"<m>Set-Cookie</m> <r>{domain}</r> <g>{name}</g> -> {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())
|