Duolingo Shop

Gets shop items

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Duolingo Shop
// @icon         https://d35aaqx5ub95lt.cloudfront.net/vendor/0e58a94dda219766d98c7796b910beee.svg
// @namespace    https://tampermonkey.net/
// @version      2.6
// @description  Gets shop items
// @author       apersongithub
// @match        *://www.duolingo.com/*
// @match        *://www.duolingo.cn/*
// @grant        none
// @license      MPL-2.0
// ==/UserScript==

/**
 * 💎 duo-gemsmith GUI - A Duolingo Reward Claim Tool 💎
 * -----------------------------------------------------------------------------------
 * Description: A GUI-based utility to claim various Duolingo rewards via API.
 *              Features categorized rewards, auto-claim functionality, and tracking.
 * Author: Modified for GUI by @apersongithub
 * Date: 2025-11-07
 *
 * DISCLAIMER: This tool is intended for educational purposes only.
 *             Use at your own risk. May violate Duolingo's Terms of Service.
 * -----------------------------------------------------------------------------------
 */

(function() {
    'use strict';

    // Reward categories with their patterns and types
    const REWARD_CATEGORIES = {
        'Daily Goals': {
            patterns: ['DAILY_GOAL', 'DAILY_GOAL_BALANCED', 'DAILY_GOAL_DOUBLE'],
            rewards: []
        },
        'Daily Quests': {
            patterns: ['DAILY_QUEST_THIRD', 'DAY_ONE_STREAK_GOAL_STARTER_REWARD'],
            rewards: []
        },
        'Skill Completion': {
            patterns: ['SKILL_COMPLETION', 'SKILL_COMPLETION_BALANCED'],
            rewards: []
        },
        'Path & Chests': {
            patterns: ['PATH_CHEST', 'CAPSTONE_COMPLETION', 'TIERED_PATH_CHEST', 'TIERED_TIMED_PATH_CHEST', 'FIRST_TIERED_PATH_CHEST', 'TIMED_PATH_CHEST'],
            rewards: []
        },
        'Streak Rewards': {
            patterns: ['STREAK_REWARD_CHEST', 'PERFECT_STREAK', 'STREAK_REWARD_ROAD_FALLBACK', 'NO_STREAK_FREEZE_CHALLENGE_REWARD'],
            rewards: []
        },
        'Social & Friends': {
            patterns: ['FRIENDS_QUEST', 'FRIENDS_QUEST_GEMS', 'FRIENDS_QUEST_XP_BOOST', 'ADD_A_FRIEND_QUEST', 'FAMILY_QUEST', 'FRIENDS_CLASH_QUEST_GEMS', 'INCENTIVIZED_FRIENDING_HEARTS', 'INCENTIVIZED_CONTACT_SYNC_GEMS'],
            rewards: []
        },
        'Events': {
            patterns: ['EVENTS_SOLO', 'EVENTS_PARTNER', 'MONTHLY_CHALLENGE', 'ARWAU'],
            rewards: []
        },
        'Sharing Rewards': {
            patterns: ['WECHAT_STREAK_SHARING', 'FIRST_TIME_SHARING_REWARD', 'RECURRING_SHARING_REWARD', 'LEADERBOARDS_RANKUP_SHARING', 'YEAR_IN_REVIEW_SHARING_REWARD'],
            rewards: []
        },
        'Shop & Videos': {
            patterns: ['SHOP_REWARDED_VIDEO', 'SHOP_REWARDED_VIDEO_BALANCED'],
            rewards: []
        },
        'Score Upgrades': {
            patterns: ['SCORE_UPGRADE', 'SCORE_UPGRADE_REDUCED', 'SCORE_UPGRADE_TIERED_CHEST'],
            rewards: []
        },
        'Time-Based': {
            patterns: ['EARLY_BIRD_CHEST', 'NIGHT_OWL_CHEST'],
            rewards: []
        },
        'Widgets & Mobile': {
            patterns: ['ADD_WIDGET_HEARTS', 'ADD_WIDGET_ENERGY_IOS'],
            rewards: []
        },
        'Other': {
            patterns: [],
            rewards: []
        }
    };

    let userId = null;
    let isClaimingAll = false;
    let claimInterval = null;

    // Parse reward data from the provided JSON
    function parseRewards(rewardBundles) {
        rewardBundles.forEach(bundle => {
            bundle.rewards.forEach(reward => {
                const rewardInfo = {
                    id: reward.id,
                    bundleType: bundle.rewardBundleType,
                    type: reward.rewardType,
                    consumed: reward.consumed,
                    amount: reward.amount || 0,
                    currency: reward.currency || '',
                    itemId: reward.itemId || '',
                    items: reward.items || [],
                    weight: reward.weight || 1,
                    isAdReward: reward.isAdReward || false
                };

                // Determine reward description
                let description = '';
                if (reward.currency && reward.amount) {
                    description = `${reward.amount} ${reward.currency}`;
                } else if (reward.itemId) {
                    description = reward.itemId.replace(/_/g, ' ');
                } else if (reward.items && reward.items.length > 0) {
                    description = reward.items.map(item =>
                        `${item.count || item.amount} ${item.itemType}`
                    ).join(', ');
                } else if (reward.amount && !reward.currency) {
                    description = `${reward.amount} Energy/Hearts`;
                } else {
                    description = reward.rewardType;
                }

                rewardInfo.description = description;

                // Categorize reward
                let categorized = false;
                for (const [category, data] of Object.entries(REWARD_CATEGORIES)) {
                    if (data.patterns.some(pattern => bundle.rewardBundleType.includes(pattern))) {
                        data.rewards.push(rewardInfo);
                        categorized = true;
                        break;
                    }
                }

                if (!categorized) {
                    REWARD_CATEGORIES['Other'].rewards.push(rewardInfo);
                }
            });
        });
    }

    // Get JWT from cookies
    function getJWTFromCookies() {
        const cookies = document.cookie.split('; ');
        for (let cookie of cookies) {
            const [name, value] = cookie.split('=');
            if (name === 'jwt_token') {
                return value;
            }
        }
        return null;
    }

    // Decode JWT to get user ID
    function decodeToken(token) {
        try {
            if (!token || !token.includes('.')) {
                throw new Error('Invalid JWT token');
            }

            const parts = token.split('.');
            if (parts.length !== 3) {
                throw new Error('Malformed JWT');
            }

            const base64Userid = parts[1];
            const padded = base64Userid + '='.repeat((4 - (base64Userid.length % 4)) % 4);
            const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
            const payload = JSON.parse(decoded);

            return payload.sub;
        } catch (err) {
            console.error(`Error decoding token: ${err.message}`);
            throw err;
        }
    }

    // Claim a reward
    async function claimReward(rewardId, fromLanguage = 'en', learningLanguage = 'fr') {
        const apiUrl = `https://www.duolingo.com/2017-06-30/users/${userId}/rewards/${rewardId}`;

        const payload = {
            consumed: true,
            fromLanguage: fromLanguage,
            learningLanguage: learningLanguage
        };

        try {
            const response = await fetch(apiUrl, {
                method: 'PATCH',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json'
                },
                body: JSON.stringify(payload)
            });

            return {
                success: response.ok,
                status: response.status,
                statusText: response.statusText
            };
        } catch (err) {
            return {
                success: false,
                error: err.message
            };
        }
    }

    // Create GUI
    function createGUI() {
        // Remove existing GUI if present
        const existing = document.getElementById('duo-gemsmith-gui');
        if (existing) existing.remove();

        // Create main container
        const gui = document.createElement('div');
        gui.id = 'duo-gemsmith-gui';
        gui.innerHTML = `
            <style>
                #duo-gemsmith-gui {
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    width: 450px;
                    max-height: 80vh;
                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                    border-radius: 15px;
                    box-shadow: 0 10px 40px rgba(0,0,0,0.3);
                    z-index: 999999;
                    font-family: 'Arial', sans-serif;
                    color: white;
                    overflow: hidden;
                }

                #duo-gemsmith-header {
                    background: rgba(0,0,0,0.2);
                    padding: 15px;
                    cursor: move;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    border-bottom: 2px solid rgba(255,255,255,0.2);
                }

                #duo-gemsmith-header h2 {
                    margin: 0;
                    font-size: 18px;
                    display: flex;
                    align-items: center;
                    gap: 8px;
                }

                #duo-gemsmith-close {
                    background: rgba(255,255,255,0.2);
                    border: none;
                    color: white;
                    width: 30px;
                    height: 30px;
                    border-radius: 50%;
                    cursor: pointer;
                    font-size: 18px;
                    transition: all 0.3s;
                }

                #duo-gemsmith-close:hover {
                    background: rgba(255,255,255,0.3);
                    transform: rotate(90deg);
                }

                #duo-gemsmith-controls {
                    padding: 15px;
                    background: rgba(0,0,0,0.1);
                    display: flex;
                    gap: 10px;
                    flex-wrap: wrap;
                }

                .duo-btn {
                    flex: 1;
                    padding: 10px 15px;
                    border: none;
                    border-radius: 8px;
                    cursor: pointer;
                    font-weight: bold;
                    font-size: 12px;
                    transition: all 0.3s;
                    min-width: 100px;
                }

                .duo-btn-primary {
                    background: #4CAF50;
                    color: white;
                }

                .duo-btn-primary:hover {
                    background: #45a049;
                    transform: translateY(-2px);
                }

                .duo-btn-danger {
                    background: #f44336;
                    color: white;
                }

                .duo-btn-danger:hover {
                    background: #da190b;
                }

                .duo-btn-warning {
                    background: #ff9800;
                    color: white;
                }

                .duo-btn-warning:hover {
                    background: #e68900;
                }

                .duo-btn:disabled {
                    opacity: 0.5;
                    cursor: not-allowed;
                }

                #duo-gemsmith-stats {
                    padding: 10px 15px;
                    background: rgba(0,0,0,0.15);
                    font-size: 12px;
                    display: flex;
                    justify-content: space-between;
                }

                #duo-gemsmith-content {
                    max-height: 50vh;
                    overflow-y: auto;
                    padding: 10px;
                }

                #duo-gemsmith-content::-webkit-scrollbar {
                    width: 8px;
                }

                #duo-gemsmith-content::-webkit-scrollbar-track {
                    background: rgba(255,255,255,0.1);
                    border-radius: 10px;
                }

                #duo-gemsmith-content::-webkit-scrollbar-thumb {
                    background: rgba(255,255,255,0.3);
                    border-radius: 10px;
                }

                #duo-gemsmith-content::-webkit-scrollbar-thumb:hover {
                    background: rgba(255,255,255,0.5);
                }

                .reward-category {
                    margin-bottom: 15px;
                    background: rgba(255,255,255,0.1);
                    border-radius: 10px;
                    overflow: hidden;
                }

                .category-header {
                    padding: 12px;
                    background: rgba(0,0,0,0.2);
                    cursor: pointer;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    font-weight: bold;
                    transition: background 0.3s;
                }

                .category-header:hover {
                    background: rgba(0,0,0,0.3);
                }

                .category-rewards {
                    max-height: 300px;
                    overflow-y: auto;
                }

                .reward-item {
                    padding: 10px 12px;
                    border-bottom: 1px solid rgba(255,255,255,0.1);
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    font-size: 11px;
                    transition: background 0.3s;
                }

                .reward-item:hover {
                    background: rgba(255,255,255,0.05);
                }

                .reward-item:last-child {
                    border-bottom: none;
                }

                .reward-info {
                    flex: 1;
                }

                .reward-name {
                    font-weight: bold;
                    margin-bottom: 3px;
                    font-size: 11px;
                }

                .reward-desc {
                    opacity: 0.8;
                    font-size: 10px;
                }

                .reward-claimed {
                    background: rgba(76, 175, 80, 0.2);
                }

                .reward-btn {
                    padding: 5px 12px;
                    border: none;
                    border-radius: 5px;
                    cursor: pointer;
                    font-size: 10px;
                    font-weight: bold;
                    transition: all 0.3s;
                }

                .claim-btn {
                    background: #2196F3;
                    color: white;
                }

                .claim-btn:hover {
                    background: #0b7dda;
                }

                .claimed-badge {
                    background: #4CAF50;
                    color: white;
                    padding: 5px 10px;
                    border-radius: 5px;
                    font-size: 10px;
                }

                .category-collapsed .category-rewards {
                    display: none;
                }

                .collapse-icon {
                    transition: transform 0.3s;
                }

                .category-collapsed .collapse-icon {
                    transform: rotate(-90deg);
                }
            </style>

            <div id="duo-gemsmith-header">
                <h2>💎 Duo-Gemsmith GUI ⛏️</h2>
                <button id="duo-gemsmith-close">×</button>
            </div>

            <div id="duo-gemsmith-controls">
                <button class="duo-btn duo-btn-primary" id="claim-all-btn">Claim All Unclaimed</button>
                <button class="duo-btn duo-btn-danger" id="stop-claim-btn" disabled>Stop Auto-Claim</button>
                <button class="duo-btn duo-btn-warning" id="refresh-btn">Refresh Data</button>
            </div>

            <div id="duo-gemsmith-stats">
                <span id="total-rewards">Total: 0</span>
                <span id="claimed-rewards">Claimed: 0</span>
                <span id="unclaimed-rewards">Unclaimed: 0</span>
            </div>

            <div id="duo-gemsmith-content"></div>
        `;

        document.body.appendChild(gui);

        // Make draggable
        makeDraggable(gui);

        // Event listeners
        document.getElementById('duo-gemsmith-close').addEventListener('click', () => {
            gui.remove();
        });

        document.getElementById('claim-all-btn').addEventListener('click', startAutoClaimAll);
        document.getElementById('stop-claim-btn').addEventListener('click', stopAutoClaim);
        document.getElementById('refresh-btn').addEventListener('click', refreshRewards);

        // Render rewards
        renderRewards();
    }

    // Render rewards in GUI
    function renderRewards() {
        const content = document.getElementById('duo-gemsmith-content');
        content.innerHTML = '';

        let totalRewards = 0;
        let claimedCount = 0;

        Object.entries(REWARD_CATEGORIES).forEach(([categoryName, categoryData]) => {
            if (categoryData.rewards.length === 0) return;

            totalRewards += categoryData.rewards.length;
            claimedCount += categoryData.rewards.filter(r => r.consumed).length;

            const categoryDiv = document.createElement('div');
            categoryDiv.className = 'reward-category';
            categoryDiv.innerHTML = `
                <div class="category-header">
                    <span>${categoryName} (${categoryData.rewards.length})</span>
                    <span class="collapse-icon">▼</span>
                </div>
                <div class="category-rewards"></div>
            `;

            const rewardsContainer = categoryDiv.querySelector('.category-rewards');

            categoryData.rewards.forEach(reward => {
                const rewardDiv = document.createElement('div');
                rewardDiv.className = `reward-item ${reward.consumed ? 'reward-claimed' : ''}`;
                rewardDiv.innerHTML = `
                    <div class="reward-info">
                        <div class="reward-name">${reward.bundleType}</div>
                        <div class="reward-desc">${reward.description}</div>
                    </div>
                    ${reward.consumed
                        ? '<span class="claimed-badge">✓ Claimed</span>'
                        : `<button class="reward-btn claim-btn" data-reward-id="${reward.id}">Claim</button>`
                    }
                `;

                if (!reward.consumed) {
                    const claimBtn = rewardDiv.querySelector('.claim-btn');
                    claimBtn.addEventListener('click', async () => {
                        claimBtn.textContent = '⏳';
                        claimBtn.disabled = true;

                        const result = await claimReward(reward.id);

                        if (result.success) {
                            reward.consumed = true;
                            claimBtn.parentElement.innerHTML = '<span class="claimed-badge">✓ Claimed</span>';
                            rewardDiv.classList.add('reward-claimed');
                            updateStats();
                        } else {
                            claimBtn.textContent = '❌ Failed';
                            setTimeout(() => {
                                claimBtn.textContent = 'Claim';
                                claimBtn.disabled = false;
                            }, 2000);
                        }
                    });
                }

                rewardsContainer.appendChild(rewardDiv);
            });

            // Toggle collapse
            categoryDiv.querySelector('.category-header').addEventListener('click', () => {
                categoryDiv.classList.toggle('category-collapsed');
            });

            content.appendChild(categoryDiv);
        });

        updateStats();
    }

    // Update statistics
    function updateStats() {
        let total = 0;
        let claimed = 0;

        Object.values(REWARD_CATEGORIES).forEach(category => {
            total += category.rewards.length;
            claimed += category.rewards.filter(r => r.consumed).length;
        });

        document.getElementById('total-rewards').textContent = `Total: ${total}`;
        document.getElementById('claimed-rewards').textContent = `Claimed: ${claimed}`;
        document.getElementById('unclaimed-rewards').textContent = `Unclaimed: ${total - claimed}`;
    }

    // Auto-claim all unclaimed rewards
    async function startAutoClaimAll() {
        if (isClaimingAll) return;

        isClaimingAll = true;
        document.getElementById('claim-all-btn').disabled = true;
        document.getElementById('stop-claim-btn').disabled = false;

        const unclaimedRewards = [];
        Object.values(REWARD_CATEGORIES).forEach(category => {
            unclaimedRewards.push(...category.rewards.filter(r => !r.consumed));
        });

        let index = 0;

        claimInterval = setInterval(async () => {
            if (!isClaimingAll || index >= unclaimedRewards.length) {
                stopAutoClaim();
                return;
            }

            const reward = unclaimedRewards[index];
            console.log(`Claiming [${index + 1}/${unclaimedRewards.length}]: ${reward.id}`);

            const result = await claimReward(reward.id);

            if (result.success) {
                reward.consumed = true;
                console.log(`✓ Successfully claimed: ${reward.description}`);
            } else {
                console.log(`✗ Failed to claim: ${reward.id}`);
            }

            index++;

            // Refresh GUI every 5 claims
            if (index % 5 === 0) {
                renderRewards();
            }
        }, Math.random() * 3000 + 2000); // Random interval between 2-5 seconds
    }

    // Stop auto-claim
    function stopAutoClaim() {
        isClaimingAll = false;
        if (claimInterval) {
            clearInterval(claimInterval);
            claimInterval = null;
        }
        document.getElementById('claim-all-btn').disabled = false;
        document.getElementById('stop-claim-btn').disabled = true;
        renderRewards();
    }

    // Refresh rewards data
    async function refreshRewards() {
        // Clear existing data
        Object.values(REWARD_CATEGORIES).forEach(category => {
            category.rewards = [];
        });

        // Fetch fresh data
        try {
            const response = await fetch(`https://www.duolingo.com/2017-06-30/users/${userId}?fields=rewardBundles`);
            const data = await response.json();

            if (data.rewardBundles) {
                parseRewards(data.rewardBundles);
                renderRewards();
                console.log('✓ Rewards refreshed successfully');
            }
        } catch (err) {
            console.error('Failed to refresh rewards:', err);
        }
    }

    // Make element draggable
    function makeDraggable(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        const header = element.querySelector('#duo-gemsmith-header');

        header.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
            element.style.right = 'auto';
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }

    // Initialize
    async function init() {
        console.log('💎 Initializing Duo-Gemsmith GUI...');

        const token = getJWTFromCookies();
        if (!token) {
            alert('⚠️ JWT token not found! Please make sure you are logged into Duolingo.');
            return;
        }

        try {
            userId = decodeToken(token);
            console.log(`✓ User ID: ${userId}`);

            // Fetch reward data
            const response = await fetch(`https://www.duolingo.com/2017-06-30/users/${userId}?fields=rewardBundles`);
            const data = await response.json();

            if (data.rewardBundles) {
                parseRewards(data.rewardBundles);
                createGUI();
                console.log('✓ Duo-Gemsmith GUI loaded successfully!');
            } else {
                alert('⚠️ Could not load reward data.');
            }
        } catch (err) {
            console.error('Error initializing:', err);
            alert(`⚠️ Error: ${err.message}`);
        }
    }

    // Start the application
    init();
})();