Steam Bundle Sites Extension

try to take over the world!

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

您需要先安裝使用者腳本管理器擴展,如 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.5.0
// @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      https://www.indiegala.com/game*
// @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'));

// inject script css
GM_addStyle(`
    pre.SBSE_errorMsg {
        height: 200px;
        text-align: left;
        white-space: pre-wrap;
    }
`);

// load up
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)];

const owned = JSON.parse(localStorage.getItem('SBSE_owned') || '{}');
const activated = {
    data: JSON.parse(GM_getValue('SBSE_activated') || '[]'),
    push(key) {
        this.data.push(key);
        GM_setValue('SBSE_activated', JSON.stringify(this.data));
    },
    check(key) {
        return this.data.includes(key);
    }
};
const config = {
    data: JSON.parse(GM_getValue('SBSE_config') || '{}'),
    set(key, value) {
        this.data[key] = value;
        GM_setValue('SBSE_config', JSON.stringify(this.data));
    },
    get(key) {
        return has.call(this.data, key) ? this.data[key] : null;
    },
    init() {
        if (!has.call(this.data, 'autoUpdateSessionID')) this.data.autoUpdateSessionID = true;
        if (!has.call(this.data, 'preselectIncludeTitle')) this.data.preselectIncludeTitle = false;
        if (!has.call(this.data, 'titleComesLast')) this.data.titleComesLast = false;
        if (!has.call(this.data, 'preselectJoinKeys')) this.data.preselectJoinKeys = false;
        if (!has.call(this.data, 'joinKeysASFStyle')) this.data.joinKeysASFStyle = true;
    }
};
const keyDetails = {
    data: {},
    set(key = '', obj) {
        if (key.length > 0) {
            obj.title = has.call(obj, 'title') ? obj.title.trim() : '';
            if (has.call(obj, 'app')) obj.app = parseInt(obj.app, 10);
            if (has.call(obj, 'sub')) obj.sub = parseInt(obj.sub, 10);
            if (has.call(obj, 'url')) {
                const matched = obj.url.match(/steam.+\/(app|sub)\/(\d+)/);

                if (matched) obj[matched[1]] = parseInt(matched[2], 10);
            }

            this.data[key] = obj;
        }
    },
    get(key) {
        return has.call(this.data, key) ? this.data[key] : null;
    },
    isOwned(key) {
        const detail = this.data[key];

        if (detail && owned.app.includes(detail.app)) return true;
        if (detail && owned.sub.includes(detail.sub)) return true;

        return false;
    }
};

config.init();

// text
const i18n = {
    tchinese: {
        name: '繁體中文',
        successStatus: '成功',
        successDetail: '無資料',
        skippedStatus: '跳過',
        activatedDetail: '已啟動',
        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: '語言',
        settingsPreselectIncludeTitle: '預選包括遊戲名',
        settingsTitleComesLast: '遊戲名置後',
        settingsPreselectJoinKeys: '預選合併序號',
        settingsJoinKeysASFStyle: '合併ASF 格式序號',
        DIGEasyBuyPurchase: '購買',
        DIGEasyBuySelectAll: '全選',
        DIGEasyBuySelectCancel: '取消',
        DIGButtonPurchasing: '購買中',
        buttonReveal: '刮開',
        buttonRetrieve: '提取',
        buttonActivate: '啟動',
        buttonCopy: '複製',
        buttonReset: '清空',
        checkboxIncludeGameTitle: '遊戲名',
        checkboxJoinKeys: '合併',
        checkboxSkipUsed: '跳過已使用',
        checkboxSkipOwned: '跳過已擁有',
        BSselectConnector: '至'
    },
    schinese: {
        name: '简体中文',
        successStatus: '成功',
        successDetail: '无信息',
        activatedDetail: '已激活',
        skippedStatus: '跳过',
        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: '语言',
        settingsPreselectIncludeTitle: '预选包括游戏名',
        settingsTitleComesLast: '游戏名置后',
        settingsPreselectJoinKeys: '预选合并激活码',
        settingsJoinKeysASFStyle: '合并ASF 格式激活码',
        DIGEasyBuyPurchase: '购买',
        DIGEasyBuySelectAll: '全选',
        DIGEasyBuySelectCancel: '取消',
        DIGButtonPurchasing: '购买中',
        buttonReveal: '刮开',
        buttonRetrieve: '提取',
        buttonActivate: '激活',
        buttonCopy: '复制',
        buttonReset: '清空',
        checkboxIncludeGameTitle: '游戏名',
        checkboxJoinKeys: '合并',
        checkboxSkipUsed: '跳过已使用',
        checkboxSkipOwned: '跳过已拥有',
        BSselectConnector: '至'
    },
    english: {
        name: 'English',
        successStatus: 'Success',
        successDetail: 'No Detail',
        activatedDetail: 'Activated',
        skippedStatus: 'Skipped',
        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',
        settingsPreselectIncludeTitle: 'Pre-select Include Title',
        settingsTitleComesLast: 'Title Comes Last',
        settingsPreselectJoinKeys: 'Pre-select Join Keys',
        settingsJoinKeysASFStyle: 'Join Keys w/ ASF Style',
        DIGEasyBuyPurchase: 'Purchase',
        DIGEasyBuySelectAll: 'Select All',
        DIGEasyBuySelectCancel: 'Cancel',
        DIGButtonPurchasing: 'Purchassing',
        buttonReveal: 'Reveal',
        buttonRetrieve: 'Retrieve',
        buttonActivate: 'Activate',
        buttonCopy: 'Copy',
        buttonReset: 'Reset',
        checkboxIncludeGameTitle: 'Game Title',
        checkboxJoinKeys: 'Join',
        checkboxSkipUsed: 'Skip Used',
        checkboxSkipOwned: 'Skip Owned',
        BSselectConnector: 'to'
    }
};
let text = has.call(i18n, config.get('language')) ? i18n[config.get('language')] : i18n.english;

// inject settings panel css
GM_addStyle(`
    .SBSE_settings .name {
        text-align: right;
        vertical-align: top;
    }
    .SBSE_settings .value {
        text-align: left;
    }
    .SBSE_settings .value > * {
        height: 30px;
        margin: 0 20px 10px;
    }
    .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.get('sessionID')}">
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsLanguage}</td>
                        <td class="value">
                            <select class="language"></select>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsPreselectIncludeTitle}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="preselectIncludeTitle">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsTitleComesLast}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="titleComesLast">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsPreselectJoinKeys}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="preselectJoinKeys">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <td class="name">${text.settingsJoinKeysASFStyle}</td>
                        <td class="value">
                            <label class="switch">
                                <input type="checkbox" class="joinKeysASFStyle">
                                <span class="slider"></span>
                            </label>
                        </td>
                    </tr>
                </table>
            </div>
        `;

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

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

        // toggles
        $panel.find('input[type=checkbox]').each((index, input) => {
            const $input = $(input);

            $input.prop('checked', config.get(input.className));
            $input.change(e => {
                swal.showLoading();

                const setting = e.delegateTarget.className;
                const state = e.delegateTarget.checked;

                config.set(setting, state);

                if (setting === 'autoUpdateSessionID') $sessionID.attr('disabled', state);

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

        // sessionID input
        $sessionID.prop('disabled', config.get('autoUpdateSessionID'));
        $sessionID.change(() => {
            swal.showLoading();

            config.set('sessionID', $sessionID.val().trim());

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

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

            const newLanguage = $language.val();
            config.set('language', newLanguage);

            text = has.call(i18n, newLanguage) ? i18n[newLanguage] : 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.check(key)) {
                self.results[key].push(`${text.skippedStatus}/${text.activatedDetail}`, text.noItemDetails);
                self.updateResults();

                // next key
                self.activateKey(callback);
            } else if (keyDetails.isOwned(key)) {
                const detail = keyDetails.get(key);
                const itemDetail = `${detail.app || detail.sub}, ${detail.title}`;

                self.results[key].push(`${text.skippedStatus}/${text.failDetailAlreadyOwned}`, itemDetail);
                self.updateResults();

                // next key
                self.activateKey(callback);
            } else {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://store.steampowered.com/account/ajaxregisterkey/',
                    headers: {
                        '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.get('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);

                                    // 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 {
                            const errorMsg = [];

                            errorMsg.push('<pre class="SBSE_errorMsg">');
                            errorMsg.push(`sessionID: ${config.get('sessionID') + eol}`);
                            errorMsg.push(`autoUpdate: ${config.get('autoUpdateSessionID') + eol}`);
                            errorMsg.push(`status: ${res.status + eol}`);
                            errorMsg.push(`response: ${res.response + eol}`);
                            errorMsg.push('</pre>');

                            swal({
                                title: text.failTitle,
                                html: text.failDetailRequestFailedNeedUpdate + eol + errorMsg.join(''),
                                type: 'error',
                                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 bundleSitesBoxHandler = {
    reveal(handler, $games) {
        const $reveal = $('.SBSE_BtnReveal');

        $reveal.addClass('working');

        handler($games, () => {
            $reveal.removeClass('working');
            $('.SBSE_BtnRetrieve').click();
        });
    },
    retrieve(data) {
        if (data.length > 0) {
            const includeTitle = !!$('.SBSE_ChkTitle:checked').length;
            const joinKeys = !!$('.SBSE_ChkJoin:checked').length;
            const separator = joinKeys ? ',' : eol;
            const prefix = joinKeys && config.get('joinKeysASFStyle') ? '!redeem ' : '';
            const keys = [];

            data.forEach(d => {
                if (typeof d === 'string') {
                    keys.push(d);
                } else {
                    const temp = [d.key];

                    if (includeTitle) temp.unshift(d.title);
                    if (config.get('titleComesLast')) temp.reverse();

                    keys.push(temp.join(', '));
                }
            });

            $('.SBSE_container > textarea').val(prefix + keys.join(separator));
        }
    },
    activate(e) {
        const $self = $(e.delegateTarget);
        const $textarea = $('.SBSE_container > textarea');
        let input = $textarea.val().trim();

        if (input.length === 0) {
            $('.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');
        });
    },
    copy() {
        $('.SBSE_container > textarea').select();
        document.execCommand('copy');
    },
    reset() {
        $('.SBSE_container > textarea').val('');
    },
    settings() {
        settings.display();
    }
};
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;
            transition: all 0.5s;
        }
        .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.narrow.working {
            width: 100px;
            padding-right: 40px;
            transition: all 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>
    `);

    // bind event
    $container.find('.SBSE_BtnCopy').click(bundleSitesBoxHandler.copy);
    $container.find('.SBSE_BtnReset').click(bundleSitesBoxHandler.reset);
    $container.find('.SBSE_BtnActivate').click(bundleSitesBoxHandler.activate);
    $container.find('#SBSE_BtnSettings').click(bundleSitesBoxHandler.settings);

    // apply settings
    if (config.get('preselectIncludeTitle')) $container.find('.SBSE_ChkTitle').prop('checked', true);
    if (config.get('preselectJoinKeys')) $container.find('.SBSE_ChkJoin').prop('checked', true);

    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(() => {
            const handler = ($games, callback) => {
                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];

                    $.ajax({
                        method: 'GET',
                        url: '/myserials/syncget',
                        dataType: 'json',
                        data: {
                            code,
                            cache: false,
                            productId: appID
                        },
                        beforeSend() {
                            $(`#permbutton_${code}, #fetchlink_${code}, #info_key_${code}`).hide();
                            $(`#fetching_${code}`).fadeIn();
                            $(`#ajax_loader_${code}`).show();
                            $(`#container_activate_${code}`).html('');
                        },
                        success(data) {
                            $(`#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($games, callback);
                        },
                        error() {
                            swal(text.failTitle, text.failDetailUnexpected, 'error');
                        }
                    });
                } else callback();
            };

            bundleSitesBoxHandler.reveal(handler, $(source).find('a[id^=fetchlink_]'));
        });
        $('.SBSE_BtnRetrieve').click(() => {
            const keys = [];

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

                if (key) {
                    const $a = $ele.find('.title_game > a');
                    const title = $a.text().trim();

                    // append key details
                    keyDetails.set(key, {
                        url: $a.attr('href'),
                        title: $a.text()
                    });

                    keys.push({
                        key,
                        title
                    });
                }
            });

            bundleSitesBoxHandler.retrieve(keys);
        });
    },
    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());

            // 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;}
            `);

            // narrow buttons
            $('.SBSE_container button').addClass('narrow');

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

            // button click
            $('.SBSE_BtnReveal').click(() => {
                const handler = ($games, callback) => {
                    const game = $games.shift();

                    if (game) {
                        if (!game.closest('.ng-hide')) {
                            game.click();
                            setTimeout(handler.bind(null, $games, callback), 300);
                        } else handler();
                    } else callback();
                };

                bundleSitesBoxHandler.reveal(handler, BSselect('.key-container a[ng-click^="redeemSerial"]'));
            });

            $('.SBSE_BtnRetrieve').click(() => {
                const keys = [];

                BSselect('.key-container input').each((index, input) => {
                    const $input = $(input);

                    keys.push({
                        key: $input.val(),
                        title: $input.closest('.key-container').prev().text().trim()
                    });
                });

                bundleSitesBoxHandler.retrieve(keys);
            });
        }

        // 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 > div { position: relative;}
            .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);
            }
            #SBSE_BtnSettings { position: absolute;}
        `);

        // narrow buttons
        $('.SBSE_container button').addClass('narrow');

        // append checkbox for owned game
        $('#SBSE_BtnSettings').before($(`<label><input type="checkbox" class="SBSE_ChkSkipOwned">${text.checkboxSkipOwned}</label>`));

        // button click
        $('.SBSE_BtnReveal').click(() => {
            const handler = ($games, callback) => {
                const game = $games.shift();

                if (game) {
                    game.click();
                    setTimeout(() => {
                        const $popup = $('.sr-warning-modal-buttons');
                        const skipOwned = !!$('.SBSE_ChkSkipOwned:checked').length;
                        const selector = skipOwned ? '.sr-warning-modal-cancel-button' : '.sr-warning-modal-confirm-button';

                        if ($popup.length > 0) $popup.find(selector).click();

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

            bundleSitesBoxHandler.reveal(handler, $('.sr-unredeemed-steam-button'));
        });
        $('.SBSE_BtnRetrieve').click(() => {
            const keys = [];

            $('.sr-redeemed-bubble').each((index, element) => {
                const $game = $(element);
                const $heading = $game.closest('[class^=sr-key]').prev().children().eq(0);

                keys.push({
                    key: $game.text().trim(),
                    title: $heading.text().trim()
                });
            });

            bundleSitesBoxHandler.retrieve(keys);
        });

        // setup key details
        let data = $('.steam-keyredeemer-container').next().text().split(eol)[3].trim().slice(11, -1);

        try {
            data = JSON.parse(data);

            data.keys.forEach(key => {
                keyDetails.set(key.redeemedKeyVal, {
                    app: key.steamAppId,
                    title: key.humanName
                });
            });
        } catch (e) {
            // no key details
        }
    },
    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;
                }
            `);

            // button click
            $('.SBSE_BtnReveal').click(() => {
                const handler = () => {
                    const $form = $('#form3');

                    $('.quickaction').val(1);
                    $.ajax({
                        method: 'POST',
                        url: $form.attr('action'),
                        data: $form.serializeArray(),
                        success() {
                            location.reload();
                        }
                    });
                };

                bundleSitesBoxHandler.reveal(handler);
            });
            $('.SBSE_BtnRetrieve').click(() => {
                const keys = [];

                $('#TableKeys tr').each((index, tr) => {
                    const $tds = $(tr).children();

                    if (tr.textContent.includes('-')) {
                        keys.push({
                            key: $tds.eq(4).text().trim(),
                            title: $tds.eq(2).text().trim()
                        });
                    }
                });

                bundleSitesBoxHandler.retrieve(keys);
            });
        } 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 = callback => {
                    var item, $item, id, price, url, requestInit, res;
                    return regeneratorRuntime.async(function _callee$(_context) {
                        while (1) switch (_context.prev = _context.next) {
                            case 0:
                                item = $checked.shift();

                                if (!item) {
                                    _context.next = 16;
                                    break;
                                }

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

                                if (!(id && price > 0 && balance - price > 0)) {
                                    _context.next = 14;
                                    break;
                                }

                                url = `${location.origin}/account_buy.html`;
                                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;
                                }

                                _context.next = 11;
                                return regeneratorRuntime.awrap(fetch(url, requestInit));

                            case 11:
                                res = _context.sent;


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

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

                            case 14:
                                _context.next = 17;
                                break;

                            case 16:
                                callback();

                            case 17:
                            case 'end':
                                return _context.stop();
                        }
                    }, null, this);
                };

                $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;}
        `);

        // narrow buttons
        $('.SBSE_container button').addClass('narrow');

        // button click
        $('.SBSE_BtnReveal').click(() => {
            const handler = ($games, callback) => {
                const game = $games.shift();

                if (game) {
                    game.click();
                    setTimeout(handler.bind(null, $games, callback), 300);
                } else callback();
            };

            bundleSitesBoxHandler.reveal(handler, $('.deliver-btn'));
        });
        $('.SBSE_BtnRetrieve').click(() => {
            const keys = [];

            $('.deliver-gkey').each((index, element) => {
                const $game = $(element);

                keys.push({
                    key: $game.text().trim(),
                    title: $game.parent().prev().text().trim()
                });
            });

            bundleSitesBoxHandler.retrieve(keys);
        });
    },
    groupees() {
        // insert textarea
        $('.container > div').eq(1).before(bundleSitesBox());

        // inject css
        GM_addStyle(`
            .SBSE_container > textarea { background-color: #EEE; border-radius: 3px;}
            .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;}
        `);

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

        // button click
        $('.SBSE_BtnReveal').click(() => {
            const handler = ($games, callback) => {
                const game = $games.shift();

                if (game) {
                    game.click();
                    setTimeout(handler.bind(null, $games, callback), 300);
                } else callback();
            };

            const $reveals = $('.reveal-product');
            const timer = $reveals.length > 0 ? 3000 : 0;

            $reveals.click();
            setTimeout(() => {
                bundleSitesBoxHandler.reveal(handler, $('.expanded .reveal'));
            }, timer);
        });
        $('.SBSE_BtnRetrieve').click(() => {
            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) {
                    keys.push({
                        key: $game.val(),
                        title: $game.closest('.details').find('h3').text().trim()
                    });
                }
            });

            bundleSitesBoxHandler.retrieve(keys);
        });

        // 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(() => {
                bundleSitesBoxHandler.retrieve(keys);
            });
        }
    }
};
const init = () => {
    if (location.hostname === 'store.steampowered.com') {
        // save sessionID
        if (g_AccountID > 0) {
            if (!config.get('sessionID') || config.get('autoUpdateSessionID')) config.set('sessionID', g_sessionID);
            if (!config.get('language')) config.set('language', g_oSuggestParams.l);
        }
        /* else {
            swal(text.notLoggedInTitle, text.notLoggedInMsg, 'error');
        }*/
    } else {
        const site = location.hostname.replace(/(www|alds|bundle)\./, '').split('.').shift();

        // check sessionID
        if (!config.get('sessionID')) {
            swal({
                title: text.missingTitle,
                text: text.missingMsg,
                type: 'question',
                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);