correcting pixiv connection functionality

This commit is contained in:
2026-02-04 12:45:31 -05:00
parent cc19bbd372
commit 2190a9c16b
8 changed files with 315 additions and 13 deletions
+251 -2
View File
@@ -6,6 +6,18 @@
let discordToken = null;
let discordTokenCapturedAt = null;
// Store for Pixiv OAuth
let pixivRefreshToken = null;
let pixivTokenCapturedAt = null;
let pixivOAuthPending = null; // { codeVerifier, tabId, resolve, reject }
// Pixiv OAuth constants (these are public - used by all Pixiv mobile apps)
const PIXIV_CLIENT_ID = 'MOBrBDS8blbauoSck0ZfDbtuzpyT';
const PIXIV_CLIENT_SECRET = 'lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj';
const PIXIV_OAUTH_URL = 'https://app-api.pixiv.net/web/v1/login';
const PIXIV_TOKEN_URL = 'https://oauth.secure.pixiv.net/auth/token';
const PIXIV_REDIRECT_URI = 'https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback';
// Track initialization state
let initialized = false;
@@ -18,6 +30,7 @@ async function ensureInitialized() {
try {
await api.init();
await loadDiscordToken();
await loadPixivToken();
initialized = true;
console.log('Extension initialized successfully');
} catch (error) {
@@ -66,6 +79,190 @@ async function saveDiscordToken(token) {
console.log('Discord token captured and saved');
}
/**
* Load Pixiv token from storage
*/
async function loadPixivToken() {
const stored = await browser.storage.local.get(['pixivRefreshToken', 'pixivTokenCapturedAt']);
if (stored.pixivRefreshToken) {
pixivRefreshToken = stored.pixivRefreshToken;
pixivTokenCapturedAt = stored.pixivTokenCapturedAt;
console.log('Loaded Pixiv refresh token from storage (captured:', pixivTokenCapturedAt, ')');
}
}
/**
* Save Pixiv refresh token to storage
*/
async function savePixivToken(token) {
pixivRefreshToken = token;
pixivTokenCapturedAt = new Date().toISOString();
await browser.storage.local.set({
pixivRefreshToken: token,
pixivTokenCapturedAt: pixivTokenCapturedAt
});
console.log('Pixiv refresh token captured and saved');
}
/**
* Generate a random code verifier for PKCE (43-128 chars, URL-safe)
*/
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
/**
* Generate code challenge from verifier (SHA-256 hash, base64url encoded)
*/
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(hash));
}
/**
* Base64 URL encode (no padding, URL-safe characters)
*/
function base64UrlEncode(buffer) {
const base64 = btoa(String.fromCharCode(...buffer));
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
/**
* Initiate Pixiv OAuth flow
* Opens a new tab for the user to log in, then captures the authorization code
*/
async function initiatePixivOAuth() {
// Generate PKCE values
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Build OAuth URL
const params = new URLSearchParams({
code_challenge: codeChallenge,
code_challenge_method: 'S256',
client: 'pixiv-android'
});
const authUrl = `${PIXIV_OAUTH_URL}?${params.toString()}`;
console.log('Initiating Pixiv OAuth flow...');
// Create a promise that will be resolved when we get the callback
return new Promise((resolve, reject) => {
// Store the pending OAuth state
pixivOAuthPending = {
codeVerifier,
resolve,
reject,
timeoutId: setTimeout(() => {
pixivOAuthPending = null;
reject(new Error('Pixiv OAuth timed out. Please try again.'));
}, 5 * 60 * 1000) // 5 minute timeout
};
// Open the auth URL in a new tab
browser.tabs.create({ url: authUrl }).then(tab => {
pixivOAuthPending.tabId = tab.id;
console.log('Opened Pixiv OAuth tab:', tab.id);
}).catch(err => {
clearTimeout(pixivOAuthPending.timeoutId);
pixivOAuthPending = null;
reject(err);
});
});
}
/**
* Exchange authorization code for tokens
*/
async function exchangePixivCode(code, codeVerifier) {
console.log('Exchanging Pixiv authorization code for tokens...');
const params = new URLSearchParams({
client_id: PIXIV_CLIENT_ID,
client_secret: PIXIV_CLIENT_SECRET,
code: code,
code_verifier: codeVerifier,
grant_type: 'authorization_code',
include_policy: 'true',
redirect_uri: PIXIV_REDIRECT_URI
});
const response = await fetch(PIXIV_TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'PixivAndroidApp/5.0.234 (Android 11; Pixel 5)'
},
body: params.toString()
});
if (!response.ok) {
const errorText = await response.text();
console.error('Pixiv token exchange failed:', response.status, errorText);
throw new Error(`Token exchange failed: ${response.status}`);
}
const data = await response.json();
console.log('Pixiv token exchange successful');
if (!data.refresh_token) {
throw new Error('No refresh token in response');
}
return data.refresh_token;
}
/**
* Listen for Pixiv OAuth callback
* Intercepts the redirect to pixiv:// scheme and extracts the authorization code
*/
browser.webRequest.onBeforeRequest.addListener(
(details) => {
// Check if this is the OAuth callback
if (!pixivOAuthPending) return;
const url = new URL(details.url);
// Check for the callback URL pattern
if (url.pathname.includes('/callback') && url.searchParams.has('code')) {
const code = url.searchParams.get('code');
console.log('Captured Pixiv OAuth callback with code');
// Get the pending OAuth state
const { codeVerifier, resolve, reject, timeoutId, tabId } = pixivOAuthPending;
pixivOAuthPending = null;
clearTimeout(timeoutId);
// Close the OAuth tab
if (tabId) {
browser.tabs.remove(tabId).catch(() => {});
}
// Exchange the code for tokens
exchangePixivCode(code, codeVerifier)
.then(refreshToken => {
savePixivToken(refreshToken);
resolve(refreshToken);
})
.catch(err => {
reject(err);
});
// Cancel the request (don't actually navigate to the callback URL)
return { cancel: true };
}
},
{ urls: ['*://app-api.pixiv.net/web/v1/users/auth/pixiv/callback*'] },
['blocking']
);
/**
* Listen for Discord API requests to capture Authorization header
*/
@@ -178,6 +375,35 @@ async function exportCookies(platformKey) {
};
}
// Pixiv - use OAuth flow to get refresh token
if (platformKey === 'pixiv') {
// If we already have a token, use it; otherwise initiate OAuth
if (!pixivRefreshToken) {
try {
await initiatePixivOAuth();
} catch (err) {
throw new Error(`Pixiv OAuth failed: ${err.message}`);
}
}
if (!pixivRefreshToken) {
throw new Error('Failed to obtain Pixiv refresh token');
}
credentialData = pixivRefreshToken;
credentialType = 'token';
// Send to backend
const result = await api.uploadCredentials(platformKey, credentialType, credentialData);
return {
success: true,
platform: platformKey,
tokenCapturedAt: pixivTokenCapturedAt,
message: result.message || 'Refresh token exported successfully'
};
}
// Other token-based platforms require manual entry
throw new Error(`${platform.name} requires manual token entry in the web UI.`);
}
@@ -216,8 +442,8 @@ async function exportAllCookies() {
for (const platformKey of platforms) {
const platform = PLATFORMS[platformKey];
// Skip token-based platforms (except Discord if we have a token)
if (platform.authType === 'token' && platformKey !== 'discord') {
// Skip token-based platforms (except Discord and Pixiv if we have tokens)
if (platform.authType === 'token' && !['discord', 'pixiv'].includes(platformKey)) {
results[platformKey] = {
success: false,
platform: platformKey,
@@ -238,6 +464,17 @@ async function exportAllCookies() {
continue;
}
// Skip Pixiv if no token captured (don't auto-trigger OAuth in bulk export)
if (platformKey === 'pixiv' && !pixivRefreshToken) {
results[platformKey] = {
success: false,
platform: platformKey,
skipped: true,
message: 'No token - click Pixiv card to authenticate'
};
continue;
}
try {
results[platformKey] = await exportCookies(platformKey);
} catch (error) {
@@ -275,6 +512,18 @@ async function getPlatformStatus() {
? `Token captured ${new Date(discordTokenCapturedAt).toLocaleString()}`
: 'Open Discord in browser to capture token'
};
} else if (key === 'pixiv') {
status[key] = {
name: platform.name,
authType: 'token',
hasToken: !!pixivRefreshToken,
tokenCapturedAt: pixivTokenCapturedAt,
hasCookies: false,
cookieCount: 0,
note: pixivRefreshToken
? `Token captured ${new Date(pixivTokenCapturedAt).toLocaleString()}`
: 'Click to authenticate with Pixiv'
};
} else {
status[key] = {
name: platform.name,
+4 -1
View File
@@ -19,13 +19,16 @@
"tabs",
"activeTab",
"webRequest",
"webRequestBlocking",
"*://*.patreon.com/*",
"*://*.subscribestar.com/*",
"*://*.subscribestar.adult/*",
"*://*.hentai-foundry.com/*",
"*://*.discord.com/*",
"*://*.pixiv.net/*",
"*://*.deviantart.com/*"
"*://*.deviantart.com/*",
"*://app-api.pixiv.net/*",
"*://oauth.secure.pixiv.net/*"
],
"browser_action": {
+15 -2
View File
@@ -165,9 +165,9 @@ function createPlatformCard(key, platform, status) {
card.className = 'platform-card';
card.dataset.platform = key;
// Discord is clickable if we have a token captured
// Discord and Pixiv are clickable (Pixiv initiates OAuth if no token)
// Other token-based platforms are disabled
const isDisabled = platform.authType === 'token' && key !== 'discord';
const isDisabled = platform.authType === 'token' && !['discord', 'pixiv'].includes(key);
const isDiscordWithoutToken = key === 'discord' && !status.hasToken;
if (isDisabled || isDiscordWithoutToken) {
@@ -236,6 +236,14 @@ function getStatusText(status, platform, key) {
return 'Open Discord in browser to capture token';
}
// Special handling for Pixiv OAuth
if (key === 'pixiv') {
if (status.hasToken) {
return 'Token captured - ready to export';
}
return 'Click to authenticate with Pixiv';
}
if (platform.authType === 'token') {
return 'Manual token entry required';
}
@@ -261,6 +269,11 @@ function getStatusClass(status, platform, key) {
return status.hasToken ? 'ready' : 'no-cookies';
}
// Special handling for Pixiv
if (key === 'pixiv') {
return status.hasToken ? 'ready' : 'no-cookies';
}
if (platform.authType === 'token') {
return 'no-cookies';
}