Steam Bundle Sites Extension

try to take over the world!

目前为 2017-09-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         Steam Bundle Sites Extension
// @namespace    http://tampermonkey.net/
// @version      1.0.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      http*://*bundlestars.com/*
// @include      https://www.humblebundle.com/downloads*
// @include      http*://*dailyindiegame.com/*
// @include      http*://bundle.ccyycn.com/order/*
// @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, 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 regKey = /([A-Za-z0-9]{5}-){2,4}[A-Za-z0-9]{5}/g;
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: '合併',
        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: '合并',
        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',
        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("\n"));
        }
    },
    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));
                                }
                            } 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').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;
            }
        `);

        // button click
        $('.SBSE_BtnReveal').click((e) => {
            const $self = $(e.delegateTarget);
            const $games = $('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 ? ',' : "\n";
            const keys = [];

            $('.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 ? ',' : "\n";
                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 ? ',' : "\n";
            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 ? ',' : "\n";
                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 ? ',' : "\n";
            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));
        });
    },
    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 ? ',' : "\n";

                $('.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);
    }
};

$(init);