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())