Steam Bundle Sites Extension

try to take over the world!

目前為 2017-09-05 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Steam Bundle Sites Extension
// @namespace    http://tampermonkey.net/
// @version      1.2.1
// @description  try to take over the world!
// @icon         http://store.steampowered.com/favicon.ico
// @author       Bisumaruko
// @include      http*://store.steampowered.com/*
// @include      https://www.indiegala.com/gift*
// @include      https://www.indiegala.com/profile*
// @include      http*://*bundlestars.com/*
// @include      https://www.humblebundle.com/downloads*
// @include      http*://*dailyindiegame.com/*
// @include      http*://bundle.ccyycn.com/order/*
// @include      https://groupees.com/purchases
// @include      http*://*agiso.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.6.6/sweetalert2.min.js
// @resource     SweetAlert2CSS https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.6.6/sweetalert2.min.css
// @connect      store.steampowered.com
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @run-at       document-start
// @noframes
// ==/UserScript==

/* global GM_xmlhttpRequest, GM_setValue, GM_getValue, GM_addStyle, GM_getResourceText,
   swal, g_AccountID, g_sessionID, g_oSuggestParams,
   window, document, location, fetch, localStorage, MutationObserver, Option */

// setup jQuery
const $ = jQuery.noConflict(true);

$.fn.pop = [].pop;
$.fn.shift = [].shift;

// inject swal css
GM_addStyle(
    GM_getResourceText('SweetAlert2CSS'),
);

// setup swal
swal.setDefaults({
    timer: 3000,
    useRejections: false,
});

const config = JSON.parse(GM_getValue('SBSE_config') || '{}');
const activated = JSON.parse(GM_getValue('SBSE_activated') || '[]');
const owned = JSON.parse(localStorage.getItem('SBSE_owned') || '{}');
const regKey = /([A-Za-z0-9]{5}-){2,4}[A-Za-z0-9]{5}/g;
const eol = "\n";
const has = Object.prototype.hasOwnProperty;
const unique = a => [...new Set(a)];

// text
const i18n = {
    tchinese: {
        name: '繁體中文',
        successStatus: '成功',
        successDetail: '無資料',
        activatedStatus: '已啟動',
        failStatus: '失敗',
        failTitle: '糟糕!',
        failDetailUnexpected: '發生未知錯誤,請稍後再試',
        failDetailInvalidKey: '序號錯誤',
        failDetailUsedKey: '序號已被使用',
        failDetailRateLimited: '啟動受限',
        failDetailCountryRestricted: '地區限制',
        failDetailAlreadyOwned: '產品已擁有',
        failDetailMissingBaseGame: '未擁有主程式',
        failDetailPS3Required: '需要PS3 啟動',
        failDetailGiftWallet: '偵測到禮物卡/錢包序號',
        failDetailParsingFailed: '處理資料發生錯誤,請稍後再試',
        failDetailRequestFailedNeedUpdate: '請求發生錯誤,請稍後再試<br>或者嘗試更新SessionID',
        noItemDetails: '無產品詳細資料',
        notLoggedInTitle: '未登入',
        notLoggedInMsg: '請登入Steam 以讓腳本紀錄SessionID',
        missingTitle: '未發現SessionID',
        missingMsg: '請問要更新SessionID 嗎?',
        emptyInput: '未發現Steam 序號',
        settingsTitle: '設定',
        settingsAutoUpdateSessionID: '自動更新SessionID',
        settingsSessionID: '我的SessionID',
        settingsLanguage: '語言',
        DIGEasyBuyPurchase: '購買',
        DIGEasyBuySelectAll: '全選',
        DIGEasyBuySelectCancel: '取消',
        DIGButtonPurchasing: '購買中',
        buttonReveal: '刮開',
        buttonRetrieve: '提取',
        buttonActivate: '啟動',
        buttonCopy: '複製',
        buttonReset: '清空',
        checkboxIncludeGameTitle: '遊戲名',
        checkboxJoinKeys: '合併',
        checkboxSkipUsed: '跳過已使用',
        BSselectConnector: '至',
    },
    schinese: {
        name: '简体中文',
        successStatus: '成功',
        successDetail: '无信息',
        activatedStatus: '已激活',
        failStatus: '失败',
        failTitle: '糟糕!',
        failDetailUnexpected: '发生未知错误,请稍后再试',
        failDetailInvalidKey: '激活码错误',
        failDetailUsedKey: '激活码已被使用',
        failDetailRateLimited: '激活受限',
        failDetailCountryRestricted: '地区限制',
        failDetailAlreadyOwned: '产品已永有',
        failDetailMissingBaseGame: '未拥有基础游戏',
        failDetailPS3Required: '需要PS3 激活',
        failDetailGiftWallet: '侦测到礼物卡/钱包激活码',
        failDetailParsingFailed: '处理资料发生错误,请稍后再试',
        failDetailRequestFailedNeedUpdate: '请求发生错误,请稍后再试<br>或者尝试更新SessionID',
        noItemDetails: '无产品详细信息',
        notLoggedInTitle: '未登入',
        notLoggedInMsg: '请登入Steam 以让脚本记录SessionID',
        missingTitle: '未发现SessionID',
        missingMsg: '请问要更新SessionID 吗?',
        emptyInput: '未批配到Steam 激活码',
        settingsTitle: '設置',
        settingsAutoUpdateSessionID: '自动更新SessionID',
        settingsSessionID: '我的SessionID',
        settingsLanguage: '语言',
        DIGEasyBuyPurchase: '购买',
        DIGEasyBuySelectAll: '全选',
        DIGEasyBuySelectCancel: '取消',
        DIGButtonPurchasing: '购买中',
        buttonReveal: '刮开',
        buttonRetrieve: '提取',
        buttonActivate: '激活',
        buttonCopy: '复制',
        buttonReset: '清空',
        checkboxIncludeGameTitle: '游戏名',
        checkboxJoinKeys: '合并',
        checkboxSkipUsed: '跳过已使用',
        BSselectConnector: '至',
    },
    english: {
        name: 'English',
        successStatus: 'Success',
        successDetail: 'No Detail',
        activatedStatus: 'Activated',
        failStatus: 'Fail',
        failTitle: 'Opps!',
        failDetailUnexpected: 'Unexpected Error',
        failDetailInvalidKey: 'Invalid Key',
        failDetailUsedKey: 'Used Key',
        failDetailRateLimited: 'Rate Limited',
        failDetailCountryRestricted: 'Country Restricted',
        failDetailAlreadyOwned: 'Product Already Owned',
        failDetailMissingBaseGame: 'Missing Base Game',
        failDetailPS3Required: 'PS3 Activation Required',
        failDetailGiftWallet: 'Gift Card/Wallet Code Detected',
        failDetailParsingFailed: 'Result parse failed',
        failDetailRequestFailedNeedUpdate: 'Request failed, please try again<br>or update sessionID',
        noItemDetails: 'No Item Details',
        notLoggedInTitle: 'Not Logged-In',
        notLoggedInMsg: 'Please login to Steam so sessionID can be saved',
        missingTitle: 'Missing SessionID',
        missingMsg: 'Do you want to update your Steam sessionID?',
        emptyInput: 'Could not find Steam code',
        settingsTitle: 'Settings',
        settingsAutoUpdateSessionID: 'Auto Update SessionID',
        settingsSessionID: 'Your sessionID',
        settingsLanguage: 'Language',
        DIGEasyBuyPurchase: 'Purchase',
        DIGEasyBuySelectAll: 'Select All',
        DIGEasyBuySelectCancel: 'Cancel',
        DIGButtonPurchasing: 'Purchassing',
        buttonReveal: 'Reveal',
        buttonRetrieve: 'Retrieve',
        buttonActivate: 'Activate',
        buttonCopy: 'Copy',
        buttonReset: 'Reset',
        checkboxIncludeGameTitle: 'Include Game Title',
        checkboxJoinKeys: 'Join Keys',
        checkboxSkipUsed: 'Skip Used',
        BSselectConnector: 'to',
    },
};
let text = has.call(i18n, config.language) ? i18n[config.language] : i18n.english;

// inject settings panel css
GM_addStyle(`
    .SBSE_settings .name {
        text-align: right;
    }
    .SBSE_settings .value {
        text-align: left;
    }
    .SBSE_settings .value > * {
        height: 30px;
        margin: 10px 20px;
    }
    .SBSE_settings .switch {
        position: relative;
        display: inline-block;
        width: 60px;
    }
    .SBSE_settings .switch input {
        display: none;
    }
    .SBSE_settings .slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #ccc;
        transition: 0.4s;
    }
    .SBSE_settings .slider:before {
        position: absolute;
        content: "";
        height: 26px;
        width: 26px;
        left: 2px;
        bottom: 2px;
        background-color: white;
        transition: 0.4s;
    }
    .SBSE_settings input:checked + .slider {
        background-color: #2196F3;
    }
    .SBSE_settings input:focus + .slider {
        box-shadow: 0 0 1px #2196F3;
    }
    .SBSE_settings input:checked + .slider:before {
        transform: translateX(30px);
    }
    .SBSE_settings > span {
        display: inline-block;
        cursor: pointer;
        color: white;
    }
`);

// functions
const settings = {
    construct() {
        const panelHTML = `
            <div class="SBSE_settings">
                <table>
                    <tr>
                        <td class="name">${text.settingsAutoUpdateSessionID}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="autoUpdateSessionID">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsSessionID}</td>
                        <td class="value">
                            <input type="text" class="sessionID" value="${config.sessionID}" disabled>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsLanguage}</td>
                        <td class="value">
                            <select class="language"></select>
                        </td>
                    </tr>
                </table>
            </div>
        `;

        return panelHTML;
    },
    display() {
        swal({
            title: text.settingsTitle,
            html: this.construct(),
            timer: null,
        });

        // apply settings
        const $panel = $(swal.getContent());
        const $autoUpdateSessionID = $panel.find('.autoUpdateSessionID');
        const $sessionID = $panel.find('.sessionID');
        const $language = $panel.find('.language');

        $autoUpdateSessionID.prop('checked', has.call(config, 'autoUpdateSessionID') ? config.autoUpdateSessionID : true);
        $autoUpdateSessionID.change(() => {
            swal.showLoading();

            const state = $panel.find('.autoUpdateSessionID:checked').length || 0;

            config.autoUpdateSessionID = state;
            GM_setValue('SBSE_config', JSON.stringify(config));

            $sessionID.attr('disabled', !!state);

            setTimeout(swal.hideLoading, 500);
        });

        $sessionID.change(() => {
            swal.showLoading();

            config.sessionID = $sessionID.val();
            GM_setValue('SBSE_config', JSON.stringify(config));

            setTimeout(swal.hideLoading, 500);
        });

        Object.keys(i18n).forEach((language) => {
            $language.append(new Option(i18n[language].name, language));
        });
        $panel.find(`option[value=${config.language}]`).prop('selected', true);
        $language.change(() => {
            swal.showLoading();

            config.language = $language.val();
            GM_setValue('SBSE_config', JSON.stringify(config));

            text = has.call(i18n, config.language) ? i18n[config.language] : i18n.english;

            setTimeout(swal.hideLoading, 500);
        });
    },
};
const activateHandler = {
    keys: [],
    results: {},
    updateResults(txt = null) {
        const $textarea = $('.SBSE_container > textarea');

        if (txt) {
            $textarea.val(txt);
        } else {
            const results = this.results;
            const parsed = [];

            Object.values(results).forEach((result) => {
                parsed.push(result.join(' | '));
            });

            $textarea.val(parsed.join(eol));
        }
    },
    getResultStatus(result) {
        let status = text.failStatus;
        let statusMsg = text.failDetailUnexpected;
        const errors = {
            14: text.failDetailInvalidKey,
            15: text.failDetailUsedKey,
            53: text.failDetailRateLimited,
            13: text.failDetailCountryRestricted,
            9: text.failDetailAlreadyOwned,
            24: text.failDetailMissingBaseGame,
            36: text.failDetailPS3Required,
            50: text.failDetailGiftWallet,
        };

        if (result.success === 1) {
            status = text.successStatus;
            statusMsg = text.successDetail;
        } else if (result.success === 2) {
            if (has.call(errors, result.purchase_result_details)) {
                statusMsg = errors[result.purchase_result_details];
            }
        }

        return `${status}/${statusMsg}`;
    },
    getResultItems(info) {
        const descriptions = [];

        if (info && info.line_items) {
            info.line_items.forEach((item) => {
                const description = [];

                if (item.packageid > 0) description.push(`sub: ${item.packageid}`);
                if (item.appid > 0) description.push(`app: ${item.appid}`);
                description.push(item.line_item_description);

                descriptions.push(description.join(' '));
            });
        }

        return descriptions.join(', ');
    },
    activateKey(callback) {
        const self = this;
        const key = self.keys.shift();

        if (key) {
            if (activated.includes(key)) {
                self.results[key].push(text.activatedStatus, text.noItemDetails);
                self.updateResults();

                // next key
                self.activateKey(callback);
            } else {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://store.steampowered.com/account/ajaxregisterkey/',
                    headers: {
                        Accept: 'text/javascript, text/html, application/xml, text/xml, */*',
                        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                        Origin: 'https://store.steampowered.com',
                        Referer: 'https://store.steampowered.com/account/registerkey',
                    },
                    data: `product_key=${key}&sessionid=${config.sessionID}`,
                    onload: (res) => {
                        if (res.status === 200) {
                            let status = '';
                            let items = '';

                            try {
                                const result = JSON.parse(res.response);

                                status = self.getResultStatus(result);
                                items = self.getResultItems(result.purchase_receipt_info);

                                // update activated
                                const failCode = result.purchase_result_details;
                                if (result.success === 1 || [14, 15, 9].includes(failCode)) {
                                    activated.push(key);
                                    GM_setValue('SBSE_activated', JSON.stringify(activated));
                                }

                                // dispatch activated event
                                $(document).trigger('activated', [key, result]);
                            } catch (e) {
                                status = `${text.failStatus}/${text.failDetailParsingFailed}`;
                                items = text.noItemDetails;
                            }
                            self.results[key].push(status, items);
                            self.updateResults();

                            // next key
                            setTimeout(self.activateKey.bind(self, callback), 2000);
                        } else {
                            swal({
                                title: text.failTitle,
                                html: text.failDetailRequestFailedNeedUpdate,
                                type: 'error',
                                timer: null,
                                showCancelButton: true,
                            }).then(() => {
                                window.open('https://store.steampowered.com/');
                            });
                            callback();
                        }
                    },
                });
            }
        } else callback();
    },
    activateKeys(input, callback) {
        const self = this;
        const keys = unique(input.match(regKey));

        if (keys.length > 0) {
            keys.forEach((key) => {
                self.results[key] = [key];
            });
            self.keys = Object.keys(self.results);
            self.updateResults();
            self.activateKey(callback);
        } else {
            self.updateResults(text.emptyInput);
            callback();
        }
    },
};
const bundleSitesBox = () => {
    GM_addStyle(`
        .SBSE_container {
            width: 100%;
            height: 200px;
            display: flex;
            flex-direction: column;
            box-sizing: border-box;
        }
        .SBSE_container > textarea {
            width: 100%;
            height: 150px;
            border: none;
            box-sizing: border-box;
            resize: none;
            outline: none;
        }
        .SBSE_container > div {
            width: 100%;
            padding-top: 5px;
            box-sizing: border-box;
        }
        .SBSE_container button {
            width: 120px;
            position: relative;
            margin-right: 10px;
            line-height: 28px;
            box-sizing: border-box;
            outline: none;
            cursor: pointer;
        }
        .SBSE_container label {
            margin-right: 10px;
        }
        #SBSE_BtnSettings {
            width: 20px;
            height: 20px;
            float: right;
            margin-right: 0;
            margin-left: 10px;
            background-color: transparent;
            background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGhlaWdodD0iMzJweCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzIgMzI7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMycHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxnIGlkPSJMYXllcl8xIi8+PGcgaWQ9ImNvZyI+PHBhdGggZD0iTTMyLDE3Ljk2OXYtNGwtNC43ODEtMS45OTJjLTAuMTMzLTAuMzc1LTAuMjczLTAuNzM4LTAuNDQ1LTEuMDk0bDEuOTMtNC44MDVMMjUuODc1LDMuMjUgICBsLTQuNzYyLDEuOTYxYy0wLjM2My0wLjE3Ni0wLjczNC0wLjMyNC0xLjExNy0wLjQ2MUwxNy45NjksMGgtNGwtMS45NzcsNC43MzRjLTAuMzk4LDAuMTQxLTAuNzgxLDAuMjg5LTEuMTYsMC40NjlsLTQuNzU0LTEuOTEgICBMMy4yNSw2LjEyMWwxLjkzOCw0LjcxMUM1LDExLjIxOSw0Ljg0OCwxMS42MTMsNC43MDMsMTIuMDJMMCwxNC4wMzF2NGw0LjcwNywxLjk2MWMwLjE0NSwwLjQwNiwwLjMwMSwwLjgwMSwwLjQ4OCwxLjE4OCAgIGwtMS45MDIsNC43NDJsMi44MjgsMi44MjhsNC43MjMtMS45NDVjMC4zNzksMC4xOCwwLjc2NiwwLjMyNCwxLjE2NCwwLjQ2MUwxNC4wMzEsMzJoNGwxLjk4LTQuNzU4ICAgYzAuMzc5LTAuMTQxLDAuNzU0LTAuMjg5LDEuMTEzLTAuNDYxbDQuNzk3LDEuOTIybDIuODI4LTIuODI4bC0xLjk2OS00Ljc3M2MwLjE2OC0wLjM1OSwwLjMwNS0wLjcyMywwLjQzOC0xLjA5NEwzMiwxNy45Njl6ICAgIE0xNS45NjksMjJjLTMuMzEyLDAtNi0yLjY4OC02LTZzMi42ODgtNiw2LTZzNiwyLjY4OCw2LDZTMTkuMjgxLDIyLDE1Ljk2OSwyMnoiIHN0eWxlPSJmaWxsOiM0RTRFNTA7Ii8+PC9nPjwvc3ZnPg==);
            background-size: contain;
            background-repeat: no-repeat;
            background-origin: border-box;
            border: none;
            vertical-align: top;
        }
    `);

    // spinner button affect
    GM_addStyle(`
        .SBSE_container button:before {
            content: '';
            position: absolute;
            right: 10px;
            margin-top: 5px;
            width: 20px;
            height: 20px;
            border: 3px solid;
            border-left-color: transparent;
            border-radius: 50%;
            box-sizing: border-box;
            opacity: 0;
            transition: opacity 0.5s;
            animation-duration: 1s;
            animation-iteration-count: infinite;
            animation-name: rotate;
            animation-timing-function: linear;
        }

        .SBSE_container button.working {
            padding-right: 20px;
            width: 120px;
            transition: padding-right 0.5s;
            transition: width 0.5s;
        }

        .SBSE_container button.working:before {
            transition-delay: 0.5s;
            transition-duration: 1s;
            opacity: 1;
        }

        @keyframes rotate {
            0% { transform: rotate(0deg);}
            100% { transform: rotate(360deg);}
        }
    `);

    const $container = $(`
        <div class="SBSE_container">
            <textarea></textarea>
            <div>
                <button class="SBSE_BtnReveal">${text.buttonReveal}</button>
                <button class="SBSE_BtnRetrieve">${text.buttonRetrieve}</button>
                <button class="SBSE_BtnActivate">${text.buttonActivate}</button>
                <button class="SBSE_BtnCopy">${text.buttonCopy}</button>
                <button class="SBSE_BtnReset">${text.buttonReset}</button>
                <label><input type="checkbox" class="SBSE_ChkTitle">${text.checkboxIncludeGameTitle}</label>
                <label><input type="checkbox" class="SBSE_ChkJoin">${text.checkboxJoinKeys}</label>
                <button id="SBSE_BtnSettings"> </button>
            </div>
        </div>
    `);

    $container.find('.SBSE_BtnCopy').click(() => {
        $('.SBSE_container > textarea').select();
        document.execCommand('copy');
    });
    $container.find('.SBSE_BtnReset').click(() => {
        $('.SBSE_container > textarea').val('');
    });
    $container.find('.SBSE_BtnActivate').click((e) => {
        const $self = $(e.delegateTarget);
        const $textarea = $('.SBSE_container > textarea');
        let input = $textarea.val().trim();

        if (input.length === 0) {
            $('.SBSE_container input:not(.keep)').prop('checked', false);
            $('.SBSE_BtnRetrieve').click();
            input = $textarea.val();
        }

        $self.prop('disabled', true).addClass('working');
        $textarea.attr('disabled', '');

        activateHandler.activateKeys(input, () => {
            $self.prop('disabled', false).removeClass('working');
            $textarea.removeAttr('disabled');
        });
    });
    $container.find('#SBSE_BtnSettings').click(() => {
        settings.display();
    });

    return $container;
};
const siteCache = {
    bundlestars: {
        doms: [document],
    },
};
const siteHandlers = {
    indiegala() {
        // insert textarea
        $('#library-contain').eq(0).before(bundleSitesBox());

        // inject css
        GM_addStyle(`
            .SBSE_container {
                margin-top: 10px;
            }
            .SBSE_container > textarea {
                border: 1px solid #CC001D;
            }
            .SBSE_container button {
                background-color: #CC001D;
                color: white;
            }
        `);

        // dom source
        const source = location.pathname === '/profile' ? 'div[id*="_sale_"].collapse.in' : document;

        // button click
        $('.SBSE_BtnReveal').click((e) => {
            const $self = $(e.delegateTarget);
            const $games = $(source).find('img[src*="steam-icon-tm-g.png"]').closest('a');
            const handler = async () => {
                const game = $games.shift();

                if (game) {
                    const $game = $(game);
                    const code = $game.attr('id').split('_').pop();
                    const appID = $game.attr('onclick').match(/steampowered\.com\/app\/(\d+)/)[1];

                    $(`#permbutton_${code}, #fetchlink_${code}, #info_key_${code}`).hide();
                    $(`#fetching_${code}`).fadeIn();
                    $(`#ajax_loader_${code}`).show();
                    $(`#container_activate_${code}`).html('');

                    const url = `https://www.indiegala.com/myserials/syncget?code=${code}&cache=false&productId=${appID}`;
                    const res = await fetch(url, {
                        method: 'GET',
                        headers: {
                            Accept: 'application/json, text/javascript, */*; q=0.01',
                        },
                        mode: 'same-origin',
                        credentials: 'same-origin',
                        cache: 'no-store',
                        referer: location.href,
                    });

                    if (res.ok) {
                        const data = await res.json();

                        $(`#ajax_loader_${code}, #fetching_${code}, #info_key_${code}`).hide();
                        $(`#serial_${code}`).fadeIn();
                        $(`#serial_n_${code}`).val(data.serial_number);
                        $game.parent().prev().find('.btn-convert-to-trade').remove();

                        handler();
                    }
                } else {
                    $self.removeClass('working');
                    $('.SBSE_BtnRetrieve').click();
                }
            };

            $self.addClass('working');
            handler();
        });
        $('.SBSE_BtnRetrieve').click(() => {
            const includeTitle = $('.SBSE_ChkTitle:checked').length;
            const separator = $('.SBSE_ChkJoin:checked').length > 0 ? ',' : eol;
            const keys = [];

            $(source).find('.game-key-string').each((index, element) => {
                const $ele = $(element);
                const key = $ele.find('.keys').val();

                if (key) {
                    const title = includeTitle ? `${$ele.find('.title_game > a').text().trim()}, ` : '';
                    keys.push(title + key);
                }
            });

            $('.SBSE_container > textarea').val(keys.join(separator));
        });
    },
    bundlestars(firstCalled) {
        const cache = siteCache.bundlestars;
        const $anchor = $('h2:contains(Order Keys)');
        const BSselect = (selector) => {
            let $results = $();
            let from = parseInt($('.SBSE_container .selectFrom').val(), 10);
            let to = parseInt($('.SBSE_container .selectTo').val(), 10);

            if ($.isNumeric(from) && $.isNumeric(to)) {
                if (from === 0 && to > 0) from = 1;
                if (from > 0 && to === 0) to = cache.doms.length - 1;

                for (let i = Math.min(from, to); i <= Math.max(from, to); i += 1) {
                    $results = $results.add(
                        $(cache.doms[i]).find(selector),
                    );
                }
            }

            return $results;
        };

        if ($('.SBSE_container').length === 0 && $anchor.length > 0) {
            // insert textarea
            $anchor.eq(0).before(bundleSitesBox());

            // insert bundlestars select
            $('.SBSE_container > div').append(`
                <select class="selectTo"></select>
                <span>${text.BSselectConnector}</span>
                <select class="selectFrom"></select>
            `);

            // inject css
            GM_addStyle(`
                .SBSE_container {
                    border: 1px solid #424242;
                    color: #999999;
                }
                .SBSE_container > textarea {
                    background-color: #303030;
                    color: #DDD;
                }
                .SBSE_container button {
                    width: 80px;
                }
                .SBSE_container button, .SBSE_container select {
                    border: 1px solid transparent;
                    background-color: #262626;
                    color: #DEDEDE;
                }
                .SBSE_container button:hover, .SBSE_container select:hover {
                    color: #A8A8A8;
                }
                .SBSE_container label {
                    color: #DEDEDE;
                }
                .SBSE_container select {
                    max-width:120px;
                    height: 30px;
                }
                .SBSE_container select, .SBSE_container span {
                    margin-right: 0;
                    margin-left: 10px;
                    float: right;
                }
                .SBSE_container span {
                    margin-top: 5px;
                }
            `);

            // button click
            $('.SBSE_BtnReveal').click((e) => {
                const $self = $(e.delegateTarget);
                const $games = BSselect('.key-container a[ng-click^="redeemSerial"]');
                const handler = () => {
                    const game = $games.shift();

                    if (game) {
                        if (!game.closest('.ng-hide')) {
                            game.click();
                            setTimeout(handler, 300);
                        } else handler();
                    } else {
                        $self.removeClass('working');
                        $('.SBSE_BtnRetrieve').click();
                    }
                };

                $self.addClass('working');
                handler();
            });

            $('.SBSE_BtnRetrieve').click(() => {
                const includeTitle = $('.SBSE_ChkTitle:checked').length;
                const separator = $('.SBSE_ChkJoin:checked').length > 0 ? ',' : eol;
                const keys = [];

                BSselect('.key-container input').each((index, input) => {
                    const $input = $(input);
                    const title = includeTitle ? `${$input.closest('.key-container').prev().text().trim()}, ` : '';
                    const key = $input.val();

                    keys.push(title + key);
                });

                $('.SBSE_container > textarea').val(keys.join(separator));
            });
        }

        // setup select
        const $selects = $('.SBSE_container select');

        $selects.empty();
        $selects.append(new Option('All', 0));
        cache.doms = [document];
        $('hr ~ div > div:not(.ng-hide)').each((index, block) => {
            const $block = $(block);
            const $bundle = $block.find('h3');
            const $tiers = $block.find('h4');

            if ($tiers.length > 1) { // bundles with multiple tiers
                $tiers.each((i, tier) => {
                    const $tier = $(tier);

                    $selects.append(
                        new Option(`${$bundle.text()} ${$tier.text()}`, cache.doms.push($tier.parent()) - 1),
                    );
                });
            } else if ($bundle.length > 0) { // bundles with single tier
                $selects.append(
                    new Option($bundle.text(), cache.doms.push($bundle.next()) - 1),
                );
            } else { // individual games
                $selects.append(
                    new Option($block.find('.title').text(), cache.doms.push($block) - 1),
                );
            }
        });

        if (firstCalled) {
            new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    mutation.removedNodes.forEach((removedNode) => {
                        if (removedNode.id === 'loading-bar-spinner') {
                            siteHandlers.bundlestars();
                        }
                    });
                });
            }).observe(document.body, {
                childList: true,
            });
        }
    },
    humblebundle() {
        // insert textarea
        $('#steam-tab').closest('.whitebox').eq(0).before(bundleSitesBox());

        // inject css
        GM_addStyle(`
            .SBSE_container > textarea {
                border: 1px solid #AAAAAA;
                color: #4a4c45;
                text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
                border-radius: 5px;
            }
            .SBSE_container button {
                width: 80px;
                border: 1px solid #808080;
                border-radius: 3px;
                background-color: #c5c5c5;
                background: linear-gradient(to top, #cacaca, #e7e7e7);
            }
        `);

        // button click
        $('.SBSE_BtnReveal').click((e) => {
            const $self = $(e.delegateTarget);
            const $games = $('.sr-unredeemed-steam-button');
            const handler = () => {
                const game = $games.shift();

                if (game) {
                    game.click();
                    setTimeout(handler, 300);
                } else {
                    $self.removeClass('working');
                    $('.SBSE_BtnRetrieve').click();
                }
            };

            $self.addClass('working');
            handler();
        });
        $('.SBSE_BtnRetrieve').click(() => {
            const includeTitle = $('.SBSE_ChkTitle:checked').length;
            const separator = $('.SBSE_ChkJoin:checked').length > 0 ? ',' : eol;
            const keys = [];

            $('.sr-redeemed-bubble').each((index, element) => {
                const $game = $(element);
                const key = $game.text().trim();
                let title = '';

                if (includeTitle) {
                    const $heading = $game.closest('[class^=sr-key]').prev().children().eq(0);
                    title = `${$heading.text().trim()}, `;
                }

                keys.push(title + key);
            });

            $('.SBSE_container > textarea').val(keys.join(separator));
        });
    },
    dailyindiegame() {
        const pathname = location.pathname;

        if (pathname.includes('/account_page')) {
            // insert textarea
            $('#TableKeys').eq(0).before(bundleSitesBox());

            // inject css
            GM_addStyle(`
                .SBSE_container {
                    padding: 5px;
                    border: 1px solid #424242;
                }
                .SBSE_container > textarea {
                    border: 1px solid #000;
                }
                .SBSE_container button {
                    border: none;
                    background-color: #FD5E0F;
                    color: rgb(49, 49, 49);
                    font-family: Ropa Sans;
                    font-size: 15px;
                    font-weight: 600;
                }
            `);

            // reveal button not neeeded
            $('.SBSE_BtnReveal').hide();

            // button click
            $('.SBSE_BtnRetrieve').click(() => {
                const includeTitle = $('.SBSE_ChkTitle:checked').length;
                const separator = $('.SBSE_ChkJoin:checked').length > 0 ? ',' : eol;
                const keys = [];

                $('#TableKeys tr').each((i, tr) => {
                    const $tds = $(tr).children();
                    const title = includeTitle ? `${$tds.eq(2).text().trim()}, ` : '';
                    const key = $tds.eq(4).text().trim();

                    if (key.includes('-')) keys.push(title + key);
                });

                $('.SBSE_container > textarea').val(keys.join(separator));
            });
        } else if (pathname === '/account_digstore.html' || pathname === '/account_trades.html') {
            // DIG EasyBuy
            GM_addStyle(`
                .DIGEasyBuy button {
                    padding: 4px 8px;
                    outline: none;
                }
                .DIGEasyBuy_checked {
                    background-color: #222;
                }
            `);

            const $target = $('#form3').closest('tr').children().eq(0);
            const $DIGEasyBuy = $(`
                <div class="DIGEasyBuy">
                    <button class="DIGButtonPurchase DIG3_Orange_15_Form">${text.DIGEasyBuyPurchase}</button>
                    <button class="DIGButtonSelectAll DIG3_Orange_15_Form">${text.DIGEasyBuySelectAll}</button>
                </div>
            `);

            $target
                .empty()
                .append($DIGEasyBuy);

            // bind button event
            $('.DIGButtonPurchase').click((e) => {
                let bought = 0;
                let balance = parseInt($('a[href^="account_transac"]').closest('div').text().slice(12), 10) || 0;
                const $self = $(e.delegateTarget);
                const $checked = $('.DIGEasyBuy_checked');
                const handler = async (callback) => {
                    const item = $checked.shift();

                    if (item) {
                        const $item = $(item);
                        const id = $item.data('id');
                        const price = parseInt($item.data('price'), 10);

                        if (id && price > 0 && (balance - price) > 0) {
                            let url = `${location.origin}/account_buy.html`;
                            const requestInit = {
                                method: 'POST',
                                headers: {
                                    'Content-Type': 'application/x-www-form-urlencoded',
                                },
                                body: `quantity=1&xgameid=${id}&xgameprice1=${price}&send=Purchase`,
                                mode: 'same-origin',
                                credentials: 'same-origin',
                                cache: 'no-store',
                                referrer: `${location.origin}/account_buy_${id}.html`,
                            };

                            if (pathname === '/account_trades.html') {
                                url = `${location.origin}/account_buytrade_${id}.html`;
                                requestInit.body = `gameid=${id}&send=Purchase`;
                                requestInit.referrer = url;
                            }

                            const res = await fetch(url, requestInit);

                            if (res.ok) {
                                $item.click();
                                bought += 1;
                                balance -= price;
                            }

                            setTimeout(handler.bind(null, callback), 300);
                        }
                    } else callback();
                };

                $self.prop('disabled', true).text(text.DIGButtonPurchasing);
                handler(() => {
                    if (bought) window.location = `${location.origin}/account_page.html`;
                    else $self.prop('disabled', false).text(text.DIGButtonPurchase);
                });
            });
            $('.DIGButtonSelectAll').click((e) => {
                const $self = $(e.delegateTarget);
                const state = !$self.data('state');

                $('.DIGEasyBuy_row').toggleClass('DIGEasyBuy_checked', state);
                $self.data('state', state);
                $self.text(state ? text.DIGEasyBuySelectCancel : text.DIGEasyBuySelectAll);
            });

            // setup row data & event
            $('a[href^="account_buy"]').each((index, element) => {
                const $game = $(element);
                const $row = $game.closest('tr');

                $row.data({
                    id: $game.attr('href').replace(/\D/g, ''),
                    price: parseInt($game.closest('td').prev().text(), 10) || 0,
                });
                $row.click(() => {
                    $row.toggleClass('DIGEasyBuy_checked');
                });
                $row.addClass('DIGEasyBuy_row');
            });
        }
    },
    ccyycn() {
        // insert textarea
        $('.featurette-divider').eq(0).after(bundleSitesBox());

        // inject css
        GM_addStyle(`
            .SBSE_container {
                width: 80%;
                margin: 0 auto;
                color: #000;
                font-size: 16px;
            }
            .SBSE_container > textarea {
                background-color: #EEE;
                box-shadow: 0 0 1px 1px rgba(204,204,204,0.5);
                border-radius: 5px;
            }
            .SBSE_container > div {
                text-align: left;
            }
            .SBSE_container button {
                width: 80px;
                border: 1px solid transparent;
                border-radius: 5px;
                background-color: #EEE;
                box-shadow: 0 0 1px 1px rgba(204,204,204,0.5);
            }
            .SBSE_container label {
                color: #EEE;
            }
        `);

        // button click
        $('.SBSE_BtnReveal').click((e) => {
            const $self = $(e.delegateTarget);
            const $games = $('.deliver-btn');
            const handler = () => {
                const game = $games.shift();

                if (game) {
                    game.click();
                    setTimeout(handler, 300);
                } else {
                    $self.removeClass('working');
                    $('.SBSE_BtnRetrieve').click();
                }
            };

            $self.addClass('working');
            handler();
        });
        $('.SBSE_BtnRetrieve').click(() => {
            const includeTitle = $('.SBSE_ChkTitle:checked').length;
            const separator = $('.SBSE_ChkJoin:checked').length > 0 ? ',' : eol;
            const keys = [];

            $('.deliver-gkey').each((index, element) => {
                const $game = $(element);
                const title = includeTitle ? `${$game.parent().prev().text().trim()}, ` : '';
                const key = $game.text().trim();

                keys.push(title + key);
            });

            $('.SBSE_container > textarea').val(keys.join(separator));
        });
    },
    groupees() {
        // insert textarea
        $('.container > div').eq(1).before(bundleSitesBox());

        // inject css
        GM_addStyle(`
            .SBSE_container {
            }
            .SBSE_container > textarea {
                background-color: #EEE;
                border-radius: 3px;
            }
            .SBSE_container > div {
            }
            .SBSE_container button {
                font-weight: bold;
                background-color: #FFF;
                border: 1px solid #CCC;
                color: #333;
            }
            .SBSE_container button:hover {
                background-color: #e6e6e6;
                border-color: #adadad;
            }
            .SBSE_container label {
            }
        `);

        // append checkbox for used-key
        $('#SBSE_BtnSettings').before(
            $(`<label><input type="checkbox" class="SBSE_ChkSkipUsed keep" checked>${text.checkboxSkipUsed}</label>`),
        );

        // button click
        $('.SBSE_BtnReveal').click((e) => {
            const $self = $(e.delegateTarget);
            const $games = $('.expanded .reveal');
            const handler = () => {
                const game = $games.shift();

                if (game) {
                    game.click();
                    setTimeout(handler, 300);
                } else {
                    $self.removeClass('working');
                    $('.SBSE_BtnRetrieve').click();
                }
            };

            $self.addClass('working');

            // reveal products first
            const $reveals = $('.reveal-product');

            if ($reveals.length > 0) {
                $reveals.click();
                setTimeout(handler, 3000);
            } else handler();
        });
        $('.SBSE_BtnRetrieve').click(() => {
            const includeTitle = $('.SBSE_ChkTitle:checked').length;
            const separator = $('.SBSE_ChkJoin:checked').length > 0 ? ',' : eol;
            const skipUsed = $('.SBSE_ChkSkipUsed:checked').length;
            const keys = [];

            $('.expanded .code').each((index, element) => {
                const $game = $(element);
                const used = $game.prev('.key-meta').find('.usage').prop('checked');

                if (!used || (used && !skipUsed)) {
                    const title = includeTitle ? `${$game.closest('.details').find('h3').text().trim()}, ` : '';
                    const key = $game.val();

                    keys.push(title + key);
                }
            });

            $('.SBSE_container > textarea').val(keys.join(separator));
        });

        // bind custom event
        $(document).on('activated', (e, key, result) => {
            if (result.success === 1 || result.purchase_result_details === 9) {
                const $game = $(`[value=${key}]`).eq(0);

                $game.prev('.key-meta').find('.usage').click();
            }
        });
    },
    agiso() {
        const keys = unique($('body').text().match(regKey));

        if (keys.length > 0) {
            // insert textarea
            $('#tabs').eq(0).prepend(bundleSitesBox());

            // inject css
            GM_addStyle(`
                .SBSE_container > textarea {
                    border: 1px solid #AAAAAA;
                }
                .SBSE_container button {
                    border: 1px solid #d3d3d3;
                    background: #e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;
                    color: #555555;
                }
                .SBSE_container button:hover {
                    border-color: #999999;
                    background: #dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;
                    color: #212121;
                }
                #SBSE_BtnSettings {
                    width: 32px !important;
                    height: 32px !important;
                }
            `);

            // remove event from agiso
            $('.SBSE_container button').click((e) => {
                e.preventDefault();
            });

            // hide reveal
            $('.SBSE_BtnReveal').hide();

            // button click
            $('.SBSE_BtnRetrieve').click(() => {
                const separator = $('.SBSE_ChkJoin:checked').length > 0 ? ',' : eol;

                $('.SBSE_container > textarea').val(keys.join(separator));
            });
        }
    },
};
const init = () => {
    if (location.hostname === 'store.steampowered.com') {
        // save sessionID
        if (g_AccountID > 0) {
            if (!config.sessionID || config.autoUpdateSessionID) config.sessionID = g_sessionID;
            if (!config.language) config.language = g_oSuggestParams.l;

            GM_setValue('SBSE_config', JSON.stringify(config));
        } else {
            swal(text.notLoggedInTitle, text.notLoggedInMsg, 'error');
        }
    } else {
        const site = location.hostname.replace(/(www|alds|bundle)\./, '').split('.').shift();

        // check sessionID
        if (!config.sessionID) {
            swal({
                title: text.missingTitle,
                text: text.missingMsg,
                type: 'question',
                timer: null,
                showCancelButton: true,
            }).then(() => {
                window.open('https://store.steampowered.com/');
            });
        }

        if (has.call(siteHandlers, site)) {
            siteHandlers[site](true);

            // update owned every 10 min
            const updateTimer = 10 * 60 * 1000;
            if (!owned.lastUpdate || owned.lastUpdate < (Date.now() - updateTimer)) {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `http://store.steampowered.com/dynamicstore/userdata/t=${Math.random()}`,
                    onload: (res) => {
                        if (res.status === 200) {
                            const data = JSON.parse(res.response);

                            owned.app = data.rgOwnedApps;
                            owned.sub = data.rgOwnedPackages;
                            owned.lastUpdate = Date.now();

                            localStorage.setItem('SBSE_owned', JSON.stringify(owned));
                        }
                    },
                });
            }
        }
    }
};

$(init);