GGather Bookmark Fixer

Intercept and log URL bookmark requests in GGather

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GGather Bookmark Fixer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Intercept and log URL bookmark requests in GGather
// @author       dudematthew
// @match        https://web.ggather.com/*
// @match        https://core.ggather.com/*
// @grant        GM_xmlhttpRequest
// @connect      *
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // Styling for console logs
    const logStyle = {
        main: 'font-size: 16px; color: #4CAF50; font-weight: bold; padding: 5px; background: #f0f0f0;',
        url: 'font-size: 14px; color: #2196F3; padding: 3px; background: #f8f8f8;',
        warning: 'font-size: 14px; color: #FF5722; font-weight: bold; padding: 3px;',
        error: 'font-size: 14px; color: #FF0000; font-weight: bold; padding: 3px;',
        debug: 'font-size: 13px; color: #9C27B0; padding: 2px;'
    };

    function debugLog(title, data = null, style = logStyle.debug) {
        console.log('%c[DEBUG] ' + title, style);
        if (data) {
            console.log('Data:', data);
            if (data instanceof Error) {
                console.log('Stack:', data.stack);
            }
        }
    }

    // Store original XHR methods
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    // Track request URLs, methods and metadata
    const requestUrls = new WeakMap();
    const requestMethods = new WeakMap();

    // Function to extract metadata
    async function extractMetadata(targetUrl) {
        debugLog('Fetching metadata for', targetUrl);

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: targetUrl,
                headers: {
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                    'User-Agent': navigator.userAgent
                },
                onload: function (response) {
                    try {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, 'text/html');

                        // 1. Find best thumbnail first
                        let thumbnail =
                            doc.querySelector('meta[property="og:image"]')?.content ||
                            doc.querySelector('meta[name="twitter:image"]')?.content ||
                            doc.querySelector('link[rel*="icon"][sizes="192x192"]')?.href ||
                            doc.querySelector('link[rel*="icon"][sizes="128x128"]')?.href ||
                            Array.from(doc.querySelectorAll('img'))
                                .find(img => img.width > 100 && img.height > 100)?.src ||
                            null;

                        // 2. Get title
                        const title = doc.querySelector('title')?.textContent ||
                            doc.querySelector('meta[property="og:title"]')?.content ||
                            null;

                        // 3. Get description
                        const description = doc.querySelector('meta[name="description"]')?.content ||
                            doc.querySelector('meta[property="og:description"]')?.content ||
                            null;

                        // Collect additional metadata
                        const html_images = Array.from(doc.querySelectorAll('img')).map(img => ({
                            src: img.src,
                            width: img.width,
                            height: img.height,
                            alt: img.alt
                        }));

                        const html_icons = Array.from(doc.querySelectorAll('link[rel*="icon"]')).map(icon => ({
                            href: icon.href,
                            rel: icon.rel,
                            sizes: icon.sizes?.value
                        }));

                        const html_og = Array.from(doc.querySelectorAll('meta[property^="og:"]')).map(og => ({
                            property: og.getAttribute('property'),
                            content: og.content
                        }));

                        const html_meta = Array.from(doc.querySelectorAll('meta')).map(meta => ({
                            name: meta.name,
                            content: meta.content
                        }));

                        const metadata = {
                            url: targetUrl,
                            thumbnail: thumbnail,  // Thumbnail first
                            title: title,         // Title second
                            description: description,  // Description third
                            html_images: html_images,
                            html_icons: html_icons,
                            html_og: html_og,
                            html_meta: html_meta,
                            headers: response.responseHeaders,
                            is_webpage: true
                        };

                        debugLog('Extracted metadata', metadata);
                        resolve(metadata);
                    } catch (e) {
                        debugLog('Failed to extract metadata', e);
                        reject(e);
                    }
                },
                onerror: function (error) {
                    debugLog('Failed to fetch URL', error);
                    reject(error);
                }
            });
        });
    }

    // Add this at the top with other constants
    let lastSuccessfulEditRequest = null;

    // Remove the CSRF token capture from XHR since we can't access those headers
    XMLHttpRequest.prototype.open = function (method, url) {
        requestUrls.set(this, url);
        requestMethods.set(this, method);
        debugLog('XHR Open', {
            method,
            url,
            isBookmark: url.includes('urlbookmark'),
            isAuth: url.includes('auth') || url.includes('login')
        });
        return originalOpen.apply(this, arguments);
    };

    // Function to find Vue instance root
    function findVueRoot() {
        // Look for Vue instance in common root element IDs
        const rootElements = ['#app', '#__nuxt', '#__layout', '#root'];
        for (const selector of rootElements) {
            const el = document.querySelector(selector);
            if (el && el.__vue__) {
                return el.__vue__.$root;
            }
        }

        // Fallback: search all elements for Vue instance
        const elements = document.getElementsByTagName('*');
        for (const el of elements) {
            if (el.__vue__) {
                return el.__vue__.$root;
            }
        }

        return null;
    }

    // Modify the callOriginalUpdate function to handle thumbnails
    async function callOriginalUpdate(bookmarkId, metadata) {
        debugLog('🎯 Starting update attempt', { bookmarkId, metadata }, logStyle.warning);

        const saveurl = Array.from(document.querySelectorAll('*'))
            .find(el => el.__vue__?.$options?.name === 'saveurl')?.__vue__;

        if (!saveurl) {
            debugLog('❌ Could not find saveurl component', null, logStyle.error);
            return;
        }

        // Extract metadata from our fetch
        const urldata = await extractMetadata(metadata.url);
        debugLog('Extracted metadata for update', urldata);

        try {
            // First set the URL and trigger validation
            saveurl.url = metadata.url;
            await saveurl.validateBasicURL();

            // Update thumbnail first
            if (urldata.thumbnail) {
                debugLog('Updating thumbnail...');
                try {
                    await saveurl.$axios({
                        url: saveurl.$store.state.api + "/edit-urlbookmark-thumb/",
                        method: "post",
                        data: {
                            url: metadata.url,
                            image_url: urldata.thumbnail,
                            thumbnail_worn: 'self'
                        }
                    });

                    // Update the store after successful upload
                    saveurl.$store.commit("eventSaveThumbChange", {
                        thumbnail: urldata.thumbnail,
                        thumbnail_worn: 'self'
                    });

                    debugLog('✅ Thumbnail update completed');
                    // Add a small delay after thumbnail update
                    await new Promise(resolve => setTimeout(resolve, 1000));
                } catch (e) {
                    debugLog('❌ Failed to update thumbnail', e, logStyle.error);
                }
            }

            // Then update title
            if (urldata.title) {
                debugLog('Updating title...');
                // Set title in urlbookmark
                saveurl.urlbookmark = {
                    pk: bookmarkId,
                    url: metadata.url,
                    title: urldata.title,
                    rating: metadata.rating,
                    owner_notes: metadata.owner_notes
                };
                await saveurl.editURLBookmark('title');
                await new Promise(resolve => setTimeout(resolve, 1000));
                debugLog('Title update completed');
            }

            // Finally update description
            if (urldata.description) {
                debugLog('Updating description...');
                // Set description in urlbookmark
                saveurl.urlbookmark = {
                    ...saveurl.urlbookmark,  // Keep previous data
                    description: urldata.description
                };
                await saveurl.editURLBookmark('description');
                debugLog('Description update completed');
            }

            debugLog('✅ All updates completed', {
                thumbnail: urldata.thumbnail,
                title: urldata.title,
                description: urldata.description
            }, logStyle.success);

        } catch (e) {
            debugLog('❌ Update failed', e, logStyle.error);
            debugLog('Component state at failure', {
                url: saveurl.url,
                urlbookmark: saveurl.urlbookmark,
                methods: Object.keys(saveurl.$options.methods)
            });
        }
    }

    // Update the bookmark creation handler to use the original update function
    XMLHttpRequest.prototype.send = async function (data) {
        const url = requestUrls.get(this);
        const method = requestMethods.get(this);

        if (url.includes('edit-urlbookmark-thumb')) {
            debugLog('🖼️ Thumbnail request intercepted', {
                url,
                method,
                data: data instanceof FormData ?
                    Object.fromEntries(data.entries()) :
                    data,
                stack: new Error().stack  // This will show us the call stack
            }, logStyle.warning);
        }

        // Detailed logging for thumbnail requests
        if (url.includes('thumb')) {
            debugLog('🖼️ Thumbnail request intercepted', {
                url,
                method,
                data: data instanceof FormData ?
                    Object.fromEntries(data.entries()) :
                    data,
                headers: Array.from(arguments)
            });

            // Store successful thumbnail requests for analysis
            this.addEventListener('load', function () {
                if (this.status === 200) {
                    debugLog('🖼️ Successful thumbnail request', {
                        response: this.responseText,
                        response: this.responseText,
                        headers: this.getAllResponseHeaders()
                    });
                }
            });
        }

        // Log all requests related to thumbnails or bookmarks
        if (url.includes('thumb') || url.includes('bookmark')) {
            debugLog('Request intercepted', {
                url,
                method,
                data,
                headers: this.getAllResponseHeaders?.(),
                requestHeaders: Array.from(arguments)
            });
        }

        try {
            const urlObj = new URL(url);

            // Handle metadata requests (keep this part)
            if (urlObj.pathname === '/api/get-urldata/') {
                const targetUrl = urlObj.searchParams.get('url');
                debugLog('URL data request for', targetUrl);

                // Extract metadata
                const urldata = await extractMetadata(targetUrl);
                debugLog('Extracted metadata', urldata);

                // Create response
                const mockResponse = {
                    urldata: urldata
                };

                debugLog('Sending URL data response', mockResponse);

                // Set response properties
                Object.defineProperties(this, {
                    'status': { value: 200, writable: true },
                    'statusText': { value: 'OK', writable: true },
                    'responseText': { value: JSON.stringify(mockResponse), writable: true },
                    'readyState': { value: 4, writable: true },
                    'response': { value: JSON.stringify(mockResponse), writable: true }
                });

                // Set headers
                this.getAllResponseHeaders = () => 'content-type: application/json';
                this.getResponseHeader = (name) => name.toLowerCase() === 'content-type' ? 'application/json' : null;

                // Trigger events
                setTimeout(() => {
                    const readyStateEvent = new Event('readystatechange');
                    const loadEvent = new Event('load');
                    this.dispatchEvent(readyStateEvent);
                    this.dispatchEvent(loadEvent);
                    debugLog('URL data response sent');
                }, 0);

                return;
            }

            // Handle successful bookmark creation
            if (urlObj.pathname === '/api/add-urlbookmark/') {
                this.addEventListener('load', function () {
                    if (this.status === 200) {
                        try {
                            const response = JSON.parse(this.responseText);
                            debugLog('Bookmark saved', response);
                            // Use original update function instead of our own
                            callOriginalUpdate(response.pk, response);
                        } catch (e) {
                            debugLog('Failed to handle save response', e);
                        }
                    }
                });
            }

            // Capture edit requests only
            if (url.includes('edit-urlbookmark') || url.includes('urlbookmark') && (method === 'PATCH' || method === 'PUT')) {
                const headers = {};
                const originalSetRequestHeader = this.setRequestHeader;

                // Capture headers being set
                this.setRequestHeader = function (name, value) {
                    headers[name] = value;
                    return originalSetRequestHeader.apply(this, arguments);
                };

                debugLog('Edit request detected', {
                    url,
                    method,
                    headers,
                    data
                });

                // Add response handler
                this.addEventListener('load', function () {
                    if (this.status === 200) {
                        lastSuccessfulEditRequest = {
                            url,
                            method,
                            headers,
                            data,
                            response: this.responseText
                        };
                        debugLog('Captured successful edit request', lastSuccessfulEditRequest);
                    }
                });
            }

            return originalSend.apply(this, arguments);
        } catch (e) {
            debugLog('Error in send override', e);
            return originalSend.apply(this, arguments);
        }
    };

    // Also intercept fetch for auth requests
    const originalFetch = window.fetch;
    window.fetch = async function (url, options) {
        if (typeof url === 'string' && (url.includes('auth') || url.includes('login'))) {
            debugLog('Fetch auth request intercepted', {
                url,
                options
            });

            const response = await originalFetch.apply(this, arguments);
            const clonedResponse = response.clone();

            try {
                const data = await clonedResponse.json();
                debugLog('Fetch auth response', {
                    url,
                    data,
                    headers: Array.from(response.headers.entries())
                });
            } catch (e) {
                debugLog('Failed to parse fetch auth response', e);
            }

            return response;
        }
        return originalFetch.apply(this, arguments);
    };

    debugLog('GGather Bookmark Fixer is active', {
        version: GM_info.script.version,
        lastUpdate: new Date().toISOString()
    }, logStyle.main);

    // Add this function to find Nuxt/Axios config
    function findAxiosConfig() {
        // Try to find Nuxt instance
        const nuxtApp = window?.__NUXT__ || window?.$nuxt;
        if (nuxtApp) {
            debugLog('Found Nuxt app', nuxtApp);

            // Try to find Axios config
            const axiosConfig = nuxtApp?.$axios?.defaults ||
                nuxtApp?.context?.$axios?.defaults ||
                window?.$axios?.defaults;

            if (axiosConfig) {
                debugLog('Found Axios config', axiosConfig);
                return axiosConfig;
            }
        }

        // Try to find Vue instance and its Axios config
        const vueRoot = findVueRoot();
        if (vueRoot) {
            const axiosConfig = vueRoot?.$axios?.defaults ||
                vueRoot?.$options?.axios?.defaults;
            if (axiosConfig) {
                debugLog('Found Axios config in Vue root', axiosConfig);
                return axiosConfig;
            }
        }

        return null;
    }

    // Call this when page loads
    document.addEventListener('DOMContentLoaded', () => {
        const axiosConfig = findAxiosConfig();
        if (axiosConfig) {
            debugLog('Will use these Axios defaults for our requests', axiosConfig);
        }
    });

})();