Steam 令牌验证器

支持自动填写 Steam 令牌验证码和批量确认交易与市场

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/469857/1218043/Steam%20%E4%BB%A4%E7%89%8C%E9%AA%8C%E8%AF%81%E5%99%A8.js

// ==UserScript==
// @name         Steam 令牌验证器
// @namespace    https://keylol.com/t652195-1-1
// @version      0.8.3
// @description  支持自动填写 Steam 令牌验证码和批量确认交易与市场
// @author       wave
// @match        http*://store.steampowered.com/*
// @match        http*://help.steampowered.com/*
// @match        http*://checkout.steampowered.com/*
// @match        http*://steamcommunity.com/*
// @exclude      http*://store.steampowered.com/login/transfer
// @exclude      http*://help.steampowered.com/login/transfer
// @exclude      http*://steamcommunity.com/login/transfer
// @exclude      http*://store.steampowered.com/login/logout/
// @exclude      http*://help.steampowered.com/login/logout/
// @exclude      http*://steamcommunity.com/login/logout/
// @exclude      http*://store.steampowered.com/widget/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_addValueChangeListener
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      steampowered.com
// @connect      steamcommunity.com
// @require      https://bundle.run/buffer@6.0.3
// @require      https://cdn.jsdelivr.net/npm/crypto-js@4.0.0/crypto-js.min.js
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAQAAAAAYLlVAAAFP0lEQVR4AdXXA5Ar2R7H8bMuPdtmnH/Gnr22bdu2bdu27bvewfO7tu2Mvw+pqene5EbTefj8ytUn9TtK0ip07p9tad5hTb2PK9xMdke7Y93pLypdaXii78KdZfmyirQL5m6r056ZsCO6OLFhoeLNMdNzfq4ihS+PnRH/yo74iZX0e3PbR2QlHsTUybIgQcRKt818RRnrYmKZSw4kyNho8vGTrynj3P91+fMOJITYaH3IsI3gy60Oa3dee/D+Fd+nwsLEScoYszpafHx82cfNjnRd2XFSx8md1jX6INlt9XomOj+rjCo998/K3f7i8pa/Oqez+ydK496vxk9LfuxEv0JNTxiwDTPGW764u0ee+Lzr55IrnnGgf/bDyqXe/2rnhZI4aPgpX3n9VX3/mv7ptjtU6RxPtCGapDy5Eaf82FZXfyTTn7p/qkqj/wJtAStD5wZasabHtNtgZmlNVRr1PxdKEl9w67cqgI3NzZoRdnovU+F78rUyL7QFGnwYxK35Sdor7ZhGp1X4ztmd2g0IcjYNT2oLVLqvwrenvFW3n3MaqCB0X6s9iElFpfiBnt7UoivwaZIKQsde2lFRXLKqcA3uoP0oJ+cdKgijW2tH2TjjUuHq3k0/l9umoL47m2jvgYnMWOXHm8oP3lAab6hwoN54I+wChW/kq5IUqODwRoHSjXtDhevM94+nnkopzofJwf3LufQd7ajjqU+/rv4P8UbW98c26tqlc7fSp3un6TXOf1mFYom5+l7BhMWQmDGRkFNvUcY3g5x774ZS6EQMjp3E69MsKrAh6Q4kInGSeOvU95R/vJt2WQg1iYxiH9lc5Y8cZBJpiM84qLEB/5dyVD0zElJiWchL9PLZQDLiIy4y/K9BrcWhbUBlLuDbLRohXjEzsKnyp+YxIfhU4h4ef2MaTahNYyaSicdLGvo4B6kDlD/VPxaCTRRnAMhlBC5Ek548BeAmSd7jjCswBYB8WiEIVZnLNpZQF0GowzMA1kSuQBQPAZiC4GQBRRTbQhRCXwDySI5UgXYAPCAKYSp6GxCEvwEwKlIFFgGwFiGBHPSKqIgwG4CtkSqwD4DxCB3xNg6hLQAfRKpANgBDEYbibRZCUwAyIlNgMh6zEWrjrTvCAACORKJAYwrxOIkg/AW920QjbAJgqfEFErhGsUKqI9ThCSXyaI2QyksAWhpdwMl+tDKIQqjCEfKAAj6knuapi4jRBbyP3AGiEYR4KlH8tbMCj55GFogihtq48XaW9ppfg6Zk4LEdCb1AXaIQr9RkI1cp4vXusodd7OIqxU4QFU6BPB6yjQGkIHgSzVpCVcg8XEg4BXLwKCCDedQljgw8LrKV1RzmBf49ZwvVECT8Alp5ANyns+YaLsSXx0xnII2JQxAjCmjl8inzKDkfAwC9zymHaGJgAa37rCMNQdhKiSKmIUgkC2hdJg6hKsWu0hgxrsAqLhPIBAThHgA7iUOMLCC4qMxkTpLD6yxHcHEOuIYgRhcoThz98W0QgosnwOeRLCBE8RK4zSROkqs57y6ERgCsimwBYScAPRAS6MR6jjEWz1X8AIAWRheos9uJaFKbIuApdRFdFgKQEcZLeu1eyp96E+2ILgsAyGEayQiCi3qcBOAV1UMuYGJOWeXPHrPNa9A2PPI4RzY38XhJWyTkpN07/57yhzdqbXN4DRvFU/Qywpi9YKFjVxXIX7+e8nfvCgmM5SAXuU0Wm2iJhBEbdXfxhgrsk+9U/MCCEzEwDqw0WcA7Kji8MaROtd3vP4xyiwGJcZe7WmPFshjfs/8Hvxyu95jGcTAAAAAASUVORK5CYII=
// ==/UserScript==

(function() {
    function bufferizeSecret(secret) {
        if (typeof secret === 'string') {
            // Check if it's hex
            if (secret.match(/[0-9a-f]{40}/i)) {
                return buffer.Buffer.from(secret, 'hex');
            } else {
                // Looks like it's base64
                return buffer.Buffer.from(secret, 'base64');
            }
        }
        return secret;
    }

    function generateAuthCode(secret, timeOffset) {
        secret = bufferizeSecret(secret);

        let time = Math.floor(Date.now() / 1000) + (timeOffset || 0);

        let b = buffer.Buffer.allocUnsafe(8);
        b.writeUInt32BE(0, 0); // This will stop working in 2038!
        b.writeUInt32BE(Math.floor(time / 30), 4);

        let hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA1, CryptoJS.lib.WordArray.create(secret));
        hmac = buffer.Buffer.from(hmac.update(CryptoJS.lib.WordArray.create(b)).finalize().toString(CryptoJS.enc.Hex), 'hex');

        let start = hmac[19] & 0x0F;
        hmac = hmac.slice(start, start + 4);

        let fullcode = hmac.readUInt32BE(0) & 0x7FFFFFFF;

        const chars = '23456789BCDFGHJKMNPQRTVWXY';

        let code = '';
        for (let i = 0; i < 5; i++) {
            code += chars.charAt(fullcode % chars.length);
            fullcode /= chars.length;
        }

        return code;
    }

    function generateConfirmationKey(identitySecret, time, tag) {
        identitySecret = bufferizeSecret(identitySecret);

        let dataLen = 8;

        if (tag) {
            if (tag.length > 32) {
                dataLen += 32;
            } else {
                dataLen += tag.length;
            }
        }

        let b = buffer.Buffer.allocUnsafe(dataLen);
        b.writeUInt32BE(0, 0);
        b.writeUInt32BE(time, 4);

        if (tag) {
            b.write(tag, 8);
        }

        let hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA1, CryptoJS.lib.WordArray.create(identitySecret));
        return hmac.update(CryptoJS.lib.WordArray.create(b)).finalize().toString(CryptoJS.enc.Base64);
    }

    function getDeviceID(steamID) {
        let salt = '';
        return "android:" + CryptoJS.SHA1(steamID.toString() + salt).toString(CryptoJS.enc.Hex).replace(/^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12}).*$/, '$1-$2-$3-$4-$5');
    }

    function generateConfirmationQueryParams(account, tag, timeOffset) {
        var time = Math.floor(Date.now() / 1000) + (timeOffset || 0);
        var key = generateConfirmationKey(account.identitySecret, time, tag);
        var deviceID = getDeviceID(account.steamID);
        return 'a=' + account.steamID + '&tag=' + tag + '&l=schinese&m=react&t=' + time + '&p=' + encodeURIComponent(deviceID) + '&k=' + encodeURIComponent(key);
    }

    function showAddAccountDialog(strTitle, strOKButton, strCancelButton, rgModalParams) {
        if (!strOKButton) {
            strOKButton = '确定';
        }
        if (!strCancelButton) {
            strCancelButton = '取消';
        }

        var $Body = $J('<form/>');
        var $AccountNameInput = $J('<input/>', {type: 'text', 'class': ''});
        var $SharedSecretInput = $J('<input/>', {type: 'text', 'class': ''});
        var $SteamIDInput = $J('<input/>', {type: 'text', 'class': ''});
        var $IdentitySecretInput = $J('<input/>', {type: 'text', 'class': ''});
        if (rgModalParams && rgModalParams.inputMaxSize) {
            $AccountNameInput.attr('maxlength', rgModalParams.inputMaxSize);
            $SharedSecretInput.attr('maxlength', rgModalParams.inputMaxSize);
            $SteamIDInput.attr('maxlength', rgModalParams.inputMaxSize);
            $IdentitySecretInput.attr('maxlength', rgModalParams.inputMaxSize);
        }
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_description'}).append('Steam 帐户名称<span data-tooltip-text="非个人资料名称,用于自动填写 Steam 令牌验证码。"> (?)</span>'));
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_input gray_bevel for_text_input fullwidth'}).append($AccountNameInput));
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_description', 'style': 'margin-top: 8px;'}).append('共享密钥<span data-tooltip-text="即 shared secret,用于生成 Steam 令牌验证码。"> (?)</span>'));
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_input gray_bevel for_text_input fullwidth'}).append($SharedSecretInput));
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_description', 'style': 'margin-top: 8px;'}).append('64 位 Steam ID<span data-tooltip-text="以“7656”开头的 17 位数字,用于确认交易与市场。"> (?)</span>'));
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_input gray_bevel for_text_input fullwidth'}).append($SteamIDInput));
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_description', 'style': 'margin-top: 8px;'}).append('身份密钥<span data-tooltip-text="即 identity secret,用于确认交易与市场。"> (?)</span>'));
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_input gray_bevel for_text_input fullwidth'}).append($IdentitySecretInput));

        var deferred = new jQuery.Deferred();
        var fnOK = function() {
            var name = $AccountNameInput.val().trim();
            var secret = $SharedSecretInput.val().trim();
            var steamID = $SteamIDInput.val().trim();
            var identitySecret = $IdentitySecretInput.val().trim();
            if (!name) {
                name = '无名氏';
            }
            if (!secret) {
                ShowAlertDialog('错误', '请输入有效的共享密钥。', '确定');
                return;
            }
            if (steamID && steamID.indexOf('7656') != 0 && steamID.length != 17) {
                ShowAlertDialog('错误', '请输入有效的 64 位 Steam ID。', '确定');
                return;
            }
            deferred.resolve(name, secret, steamID, identitySecret);
        };
        var fnCancel = function() {
            deferred.reject();
        };

        $Body.submit(function(event) {
            event.preventDefault();
            fnOK();
        });

        var $OKButton = _BuildDialogButton(strOKButton, true);
        $OKButton.click(fnOK);
        var $CancelButton = _BuildDialogButton(strCancelButton);
        $CancelButton.click(fnCancel);

        var Modal = _BuildDialog(strTitle, $Body, [$OKButton, $CancelButton], fnCancel);
        if(!rgModalParams || !rgModalParams.bNoPromiseDismiss) {
            deferred.always(function() {
                Modal.Dismiss();
            });
        }

        Modal.Show();

        $AccountNameInput.focus();

        // attach the deferred's events to the modal
        deferred.promise(Modal);

        return Modal;
    }

    function showImportAccountDialog(strTitle, strDescription, strOKButton, strCancelButton, textAreaMaxLength) {
        if (!strOKButton) {
            strOKButton = '确定';
        }
        if (!strCancelButton) {
            strCancelButton = '取消';
        }

        var $Body = $J('<form/>');
        var $TextArea = $J('<textarea/>', {'class': 'newmodal_prompt_textarea'});
        $TextArea.attr('placeholder', strDescription);
        if (textAreaMaxLength) {
            $TextArea.attr('maxlength', textAreaMaxLength);
            $TextArea.bind('keyup change', function() {
                var str = $J(this).val();
                var mx = parseInt($J(this).attr('maxlength'));
                if (str.length > mx) {
                    $J(this).val(str.substr(0, mx));
                    return false;
                }
            });
        }
        $Body.append($J('<div/>', {'class': 'newmodal_prompt_with_textarea gray_bevel fullwidth'}).append($TextArea));

        var deferred = new jQuery.Deferred();
        var fnOK = function() {
            deferred.resolve($TextArea.val());
        };
        var fnCancel = function() {
            deferred.reject();
        };

        $Body.submit(function(event) {
            event.preventDefault();
            fnOK();
        });

        var $OKButton = _BuildDialogButton(strOKButton, true);
        $OKButton.click(fnOK);
        var $CancelButton = _BuildDialogButton(strCancelButton);
        $CancelButton.click(fnCancel);

        var Modal = _BuildDialog(strTitle, $Body, [$OKButton, $CancelButton], fnCancel);
        deferred.always(function() {
            Modal.Dismiss();
        });
        Modal.Show();

        $TextArea.focus();

        // attach the deferred's events to the modal
        deferred.promise(Modal);

        return Modal;
    }

    function showConfirmationDialog(account, confList) {
        var strOKButton = '全选';
        var strCancelButton = '关闭';

        var $Body = $J('<div/>', {'style': 'position: relative; overflow: hidden; padding-bottom: 66px;'});
        var $MobileconfList = $J('<div/>', {'id': 'mobileconf_list'});
        $J.each(confList, function(i, v) {
            var $ConfirmationEntry = $J('<div/>', {'class': 'mobileconf_list_entry', 'id': 'conf' + v.id, 'data-confid': v.id, 'data-key': v.nonce});
            var $ConfirmationEntryContent = $J('<div/>', {'class': 'mobileconf_list_entry_content'});
            var $ConfirmationEntryIcon = $J('<div/>', {'class': 'mobileconf_list_entry_icon'}).append('<img src="' + v.icon + '"/>');
            var $ConfirmationEntryDescription = $J('<div/>', {'class': 'mobileconf_list_entry_description'}).append('<div>' + v.headline + '</div><div>' + v.summary.join('<br/>') + '</div><div>' + v.type_name + ' - ' + new Date(v.creation_time * 1000).toLocaleString() + '</div>');
            var $ConfirmationEntryCheckbox = $J('<div/>', {'class': 'mobileconf_list_checkbox'}).append($J('<input/>', {'id': 'multiconf_' + v.id, 'data-confid': v.id, 'data-key': v.nonce, 'value': '1', 'type': 'checkbox'}));
            var $ConfirmationEntrySep = $J('<div/>', {'class': 'mobileconf_list_entry_sep'});
            $MobileconfList.append($ConfirmationEntry.append($ConfirmationEntryContent.append($ConfirmationEntryIcon, $ConfirmationEntryDescription, $ConfirmationEntryCheckbox), $ConfirmationEntrySep));
            $ConfirmationEntry.on('click', function() {
                window.open('https://steamcommunity.com/mobileconf/detailspage/' + v.id + '?' + generateConfirmationQueryParams(account, 'details' + v.id, timeOffset), '_blank', 'height=790,width=600,resize=yes,scrollbars=yes');
            });
            $ConfirmationEntryCheckbox.on('click', function(e) {
                e.stopPropagation();

                var nChecked = $J('.mobileconf_list_checkbox input:checked').length;
                var $elButtons = $J('#mobileconf_buttons');

                if (nChecked > 0) {
                    var $btnCancel = $J('#mobileconf_buttons .mobileconf_button_cancel');
                    var $btnAccept = $J('#mobileconf_buttons .mobileconf_button_accept');
                    $btnCancel.unbind();
                    $btnAccept.unbind();

                    $btnCancel.text('取消选择');
                    $btnAccept.text('确认已选择');

                    $btnCancel.click(function() {
                        sendMultiMobileConfirmationOp(account, 'cancel', Modal);
                    });

                    $btnAccept.click(function() {
                        sendMultiMobileConfirmationOp(account, 'allow', Modal);
                    });

                    if ($elButtons.is(':hidden')) {
                        $elButtons.css('bottom', -$elButtons.height() + 'px');
                        $elButtons.show();
                        }
                    $elButtons.css('bottom', '0');
                } else {
                    $elButtons.css('bottom', -$elButtons.height() + 'px');
                }
            });
        });
        var $MobileconfButtons = $J('<div/>', {'id': 'mobileconf_buttons', 'style': 'display: none;'});
        var $MobileconfButtonCancel = $J('<div/>', {'class': 'mobileconf_button mobileconf_button_cancel'});
        var $MobileconfButtonAccept = $J('<div/>', {'class': 'mobileconf_button mobileconf_button_accept'});
        $MobileconfButtons.append($J('<div/>').append($MobileconfButtonCancel, $MobileconfButtonAccept));

        $Body.append($MobileconfList, $MobileconfButtons);

        var deferred = new jQuery.Deferred();
        var fnOK = function() {
            $J('.mobileconf_list_checkbox input:not(:checked)').prop('checked', true);
            $J('.mobileconf_list_checkbox').eq(0).click();
        };
        var fnCancel = function() {
            deferred.reject();
        };

        var $OKButton = _BuildDialogButton(strOKButton, true);
        $OKButton.click(fnOK);
        var $CancelButton = _BuildDialogButton(strCancelButton);
        $CancelButton.click(fnCancel);

        var Modal = _BuildDialog('确认交易与市场', $Body, [$OKButton, $CancelButton], fnCancel);
        deferred.always(function() {
            Modal.Dismiss();
        });
        Modal.Show();

        // attach the deferred's events to the modal
        deferred.promise(Modal);

        return Modal;
    }

    function addAccount() {
        showAddAccountDialog('添加账户', '确定', '取消').done(function(name, secret, steamID, identitySecret) {
            if (steamID && identitySecret) {
                accounts.push({
                    name,
                    secret,
                    steamID,
                    identitySecret
                });
                GM_setValue('accounts', accounts);
                ShowAlertDialog('添加账户', '添加成功,该账户支持确认交易与市场。', '确定');
            } else {
                accounts.push({
                    name,
                    secret
                });
                GM_setValue('accounts', accounts);
                ShowAlertDialog('添加账户', '添加成功,该账户不支持确认交易与市场。', '确定');
            }
        });
        setupTooltips($J('.newmodal'));
    }

    function importAccount() {
        showImportAccountDialog('导入账户', '将要导入的数据粘贴于此', '确定', '取消').done(function(data) {
            try {
                data = JSON.parse(data.replace(/("SteamID":)(\d+)/, '$1"$2"'));
                var name = data.account_name || '无名氏';
                var secret = data.shared_secret;
                var steamID = data.steamid || data.Session && data.Session.SteamID || '';
                var identitySecret = data.identity_secret;
                if (!secret) {
                    ShowAlertDialog('错误', '共享密钥不存在,请检查后再试。', '确定').done(function() {
                        importAccount();
                    });
                    return;
                }
                if (steamID && identitySecret) {
                    accounts.push({
                        name,
                        secret,
                        steamID,
                        identitySecret
                    });
                    GM_setValue('accounts', accounts);
                    ShowAlertDialog('导入账户', '导入成功,该账户支持确认交易与市场。', '确定');
                } else {
                    accounts.push({
                        name,
                        secret
                    });
                    GM_setValue('accounts', accounts);
                    ShowAlertDialog('导入账户', '导入成功,该账户不支持确认交易与市场。', '确定');
                }
            } catch (err) {
                ShowAlertDialog('错误', '数据格式有误,请检查后再试。', '确定').done(function() {
                    importAccount();
                });
            }
        });
    }

    function deleteAccount(elem) {
        ShowConfirmDialog('删除账户', '确定删除该账户吗?', '确定', '取消').done(function() {
            var $Elem = $JFromIDOrElement(elem);
            if ($Elem.data('id') >= accounts.length) {
                ShowAlertDialog('错误', '无法删除该账户,请稍后再试。', '确定').done(function() {
                    window.location.reload();
                });
            } else {
                var account = accounts.splice($Elem.data('id'), 1)[0];
                GM_setValue('accounts', accounts);
                ShowAlertDialog('删除账户', '删除成功。', '确定');
            }
        });
    }

    function copyAuthCode(elem) {
        var $Elem = $JFromIDOrElement(elem);
        GM_setClipboard(generateAuthCode(accounts[$Elem.data('id')].secret, timeOffset));
        $Elem.css('width', window.getComputedStyle(elem, null).width).text('复制成功').addClass('copy_success');
        setTimeout(function() {
            $Elem.text($Elem.data('name')).removeClass('copy_success');
        }, 1000);
    }

    function refreshAccounts() {
        $AuthenticatorPopupMenu.empty();
        $J.each(accounts, function(i, v) {
            var $AuthenticatorPopupMenuItem = $J('<span/>', {'style': 'display: block; padding: 5px 0 5px 12px; margin-right: 27px; min-width: 50px;', 'data-id': i, 'data-name': v.name, 'data-tooltip-text': '点击复制该账户的验证码'}).append(v.name);
            var $AuthenticatorDeleteAccount = $J('<span/>', {'class': 'delete_account', 'data-id': i, 'data-tooltip-text': '删除该账户'});
            $AuthenticatorPopupMenu.append($J('<a/>', {'class': 'popup_menu_item', 'style': 'position: relative; padding: 0;'}).append($AuthenticatorPopupMenuItem, $AuthenticatorDeleteAccount));
            $AuthenticatorPopupMenuItem.on('click', function() {
                copyAuthCode(this);
            });
            $AuthenticatorDeleteAccount.on('click', function() {
                deleteAccount(this);
            });
        });
        setupTooltips($AuthenticatorPopupMenu);

        var $AuthenticatorAddAccount = $J('<a/>', {'class': 'popup_menu_item'}).append('添加账户');
        $AuthenticatorPopupMenu.append($AuthenticatorAddAccount);
        $AuthenticatorAddAccount.on('click', function() {
            addAccount();
        });

        var $AuthenticatorImportAccount = $J('<a/>', {'class': 'popup_menu_item'}).append('导入账户');
        $AuthenticatorPopupMenu.append($AuthenticatorImportAccount);
        $AuthenticatorImportAccount.on('click', function() {
            importAccount();
        });
    }

    function createConfirmationLink(steamID) {
        if (!$AuthenticatorPopupMenu.find('.confirmation').length) {
            $J.each(accounts, function(i, v) {
                if (v.steamID && steamID == v.steamID) {
                    var $AuthenticatorConfirmation = $J('<a/>', {'class': 'popup_menu_item confirmation'}).append('确认交易与市场');
                    $AuthenticatorPopupMenu.append($AuthenticatorConfirmation);
                    $AuthenticatorConfirmation.on('click', async function() {
                        var waitDialog = ShowBlockingWaitDialog('确认交易与市场', '正在获取确认信息,请稍候…');

                        try {
                            var res = await new Promise((resolve, reject) => {
                                GM_xmlhttpRequest({
                                    method: 'GET',
                                    url: 'https://steamcommunity.com/mobileconf/getlist?' + generateConfirmationQueryParams(v, 'conf', timeOffset),
                                    responseType: 'json',
                                    onload: function(response) {
                                        resolve(response.response);
                                    },
                                    onerror: function(error) {
                                        reject(error);
                                    }
                                });
                            });
                            if (res && res.success) {
                                if (res.conf && res.conf.length) {
                                    showConfirmationDialog(v, res.conf);
                                } else {
                                    ShowAlertDialog('确认交易与市场', '您当前没有任何确认信息。', '确定');
                                }
                            } else {
                                ShowAlertDialog('错误', res && res.message || '获取确认信息失败,请稍后再试。', '确定');
                            }
                        } catch (err) {
                            ShowAlertDialog('错误', '获取确认信息失败,请稍后再试。', '确定');
                        }

                        waitDialog.Dismiss();
                    });
                    return false;
                }
            });
        }
    }

    async function sendMultiMobileConfirmationOp(account, op, modal) {
        var $rgChecked = $J('.mobileconf_list_checkbox input:checked');
        if ($rgChecked.length == 0) {
            return;
        }

        var waitDialog = ShowBlockingWaitDialog('确认交易与市场', '正在执行此操作,请稍候…');

        var rgConfirmationId = [];
        var rgConfirmationKey = [];

        $J.each($rgChecked, function(key) {
            var $this = $J(this);
            var nConfirmationId = $this.data('confid');
            var nConfirmationKey = $this.data('key');

            rgConfirmationId.push(nConfirmationId);
            rgConfirmationKey.push(nConfirmationKey);
        });

        var queryString = 'op=' + op + '&' + generateConfirmationQueryParams(account, op, timeOffset);

        for (var i = 0; i < rgConfirmationId.length; i++) {
            queryString += '&cid[]=' + rgConfirmationId[i];
            queryString += '&ck[]=' + rgConfirmationKey[i];
        }

        try {
            var res = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://steamcommunity.com/mobileconf/multiajaxop',
                    data: queryString,
                    headers:    {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    },
                    responseType: 'json',
                    onload: function(response) {
                        resolve(response.response);
                    },
                    onerror: function(error) {
                        reject(error);
                    }
                });
            });
            if (res && res.success) {
                for (var j = 0; j < rgConfirmationId.length; j++) {
                    $J('#conf' + rgConfirmationId[j]).remove();
                }

                var nChecked = $J('.mobileconf_list_checkbox input:checked').length;
                var $elButtons = $J('#mobileconf_buttons');

                if (nChecked == 0) {
                    $elButtons.css('bottom', -$elButtons.height() + 'px');
                }

                if ($J('.mobileconf_list_entry').length == 0) {
                    modal && modal.Dismiss();
                } else {
                    modal && modal.AdjustSizing();
                }
            } else {
                ShowAlertDialog('确认错误', res && res.message || '执行此操作时出现问题。请稍后再重试您的请求。', '确定');
            }
        } catch (err) {
            ShowAlertDialog('确认错误', '执行此操作时出现问题。请稍后再重试您的请求。', '确定');
        }

        waitDialog.Dismiss();
    }

    function setupTooltips(selector) {
        if (window.location.hostname == 'store.steampowered.com' || window.location.hostname == 'checkout.steampowered.com') {
            BindTooltips(selector, {tooltipCSSClass: 'store_tooltip'});
        } else if (window.location.hostname == 'help.steampowered.com') {
            BindTooltips(selector, {tooltipCSSClass: 'help_tooltip'});
        } else if (window.location.hostname == 'steamcommunity.com') {
            BindTooltips(selector, {tooltipCSSClass: 'community_tooltip'});
        }
    }

    GM_addStyle(`
        .delete_account {
            position: absolute;
            right: 0;
            top: 0;
            padding: 5px 7.5px;
            width: 12px;
            height: calc(100% - 10px);
            background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAQAAAD8fJRsAAAAkElEQVR4AXWQxWEDMRBFJ6AWArqGmW7G12HMDN0ZFmr4dqKF00rDPGPhycnr/vi9nJVPl2qI7Dd0WZpZEyFEygKhy1CkPsX4JCLlB6OP6jo3eRHxhh3xA+OBLULedCtExDOGcRvM6DZzpP/RxgtR4fDKat/ylPUKpZwao1A769VBDbls3H5WO6KfjVu5YOVJDkyDcoTnvnKRAAAAAElFTkSuQmCC);
            background-position: center;
            background-repeat: no-repeat;
            background-origin: content-box;
            cursor: pointer;
        }

        .copy_success {
            color: #57cbde !important;
        }

        #mobileconf_list {
            overflow-y: auto;
            max-width: 600px;
            max-height: calc(100vh - 270px);
        }

        .mobileconf_list_entry {
            cursor: default;
            font-size: 15px;
            text-shadow: none;
            width: 100%;
            -webkit-transition: opacity 0.1s, top 0.4s;
            transition: opacity 0.1s, top 0.4s;
        }

        .mobileconf_list_entry.copy {
            -webkit-transition: opacity 0.4s, top 0.4s;
            transition: opacity 0.4s, top 0.4s;
        }

        .mobileconf_list_entry_content {
            display: flex;
            padding: 10px 20px;
        }

        .mobileconf_list_entry_icon {
            height: 3.5em;
            margin-right: 10px;
        }

        .mobileconf_list_entry_icon > img {
            width: 32px;
        }

        .mobileconf_list_entry_description {
            flex: 1;
            min-width: 0;
            margin-right: 10px;
        }

        .mobileconf_list_entry_description > div {
            color: #7a7a7a;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
        }

        .mobileconf_list_entry_description > div:first-of-type {
            color: white;
        }

        .mobileconf_list_checkbox {
            overflow: hidden;
            vertical-align: middle;
            display: inline-block;
            color: #ffffff;
            width: 3em;
            height: 3em;
            transform: scale(1.5);
            transition-property: transform;
            transition-duration: 0.1s;
        }

        .mobileconf_list_checkbox input {
            width: 3em;
            height: 3em;
        }

        .mobileconf_list_entry_sep {
            border-top: 1px solid #303030;
        }

        #mobileconf_buttons {
            position: absolute;
            display: inline-block;
            vertical-align: middle;
            height: 3.3em;
            line-height: 3.3em;
            width: 100%;
            font-size: 20px;
            background: #324056;
            background-image: radial-gradient(
                ellipse farthest-corner at 50% 0px,
                rgb(74, 107, 152) 0%,
                transparent 50%
            );
            bottom: -3em;
            left: 0;
            z-index: 10;
            -webkit-transition-property: bottom;
            transition-property: bottom;
            -webkit-transition-duration: 0.4s;
            transition-duration: 0.4s;
        }

        .mobileconf_button {
            width: 50%;
            display: inline-block;
            overflow: hidden;
            text-align: center;
            vertical-align: middle;
            line-height: normal;
            cursor: pointer;
        }
    `);

    var $GlobalActionMenu = $J('#global_action_menu');
    var $AuthenticatorLink = $J('<span/>', {'class': 'pulldown global_action_link', 'style': 'display: inline-block; padding-left: 4px; line-height: 25px;', 'onclick': 'ShowMenu(this, "authenticator_dropdown", "right");'}).append('Steam 令牌验证器');
    var $AuthenticatorDropdown = $J('<div/>', {'class': 'popup_block_new', 'id': 'authenticator_dropdown', 'style': 'display: none;'});
    var $AuthenticatorPopupMenu = $J('<div/>', {'class': 'popup_body popup_menu'});

    $GlobalActionMenu.prepend($AuthenticatorDropdown.append($AuthenticatorPopupMenu));
    $GlobalActionMenu.prepend($AuthenticatorLink);

    var accounts = GM_getValue('accounts') || [];

    refreshAccounts();

    GM_addValueChangeListener('accounts', function(name, old_value, new_value, remote) {
        accounts = new_value;
        refreshAccounts();

        if (userSteamID) {
            $AuthenticatorPopupMenu.find('.confirmation').remove();
            createConfirmationLink(userSteamID);
        }

        AlignMenu($AuthenticatorLink, 'authenticator_dropdown', 'right');
    });

    if (window.location.pathname == '/mobileconf/conf') {
        let account;
        $J.each(accounts, function(i, v) {
            if (v.steamID && g_steamID == v.steamID) {
                account = v;
                return false;
            }
        });

        unsafeWindow.GetValueFromLocalURL = function(url, timeout, success, error, fatal) {
            if (url.indexOf('steammobile://steamguard?op=conftag&arg1=allow') !== -1) {
                success(generateConfirmationQueryParams(account, 'allow', timeOffset));
            } else if (url.indexOf('steammobile://steamguard?op=conftag&arg1=cancel') !== -1) {
                success(generateConfirmationQueryParams(account, 'cancel', timeOffset));
            } else if (url.indexOf('steammobile://steamguard?op=conftag&arg1=details') !== -1) {
                success(generateConfirmationQueryParams(account, 'details', timeOffset));
            }
        };

        $J('html').removeClass('force_desktop').addClass('responsive');
        V_SetCookie('strResponsiveViewPrefs', null, -1);

        $J('.mobileconf_list_entry').each(function() {
            var $this = $J(this);
            if (!$this.has('.mobileconf_list_checkbox').length) {
                var $ConfirmationEntryCheckbox = $J('<div/>', {'class': 'mobileconf_list_checkbox'}).append($J('<input/>', {'id': 'multiconf_' + $this.data('confid'), 'data-confid': $this.data('confid'), 'data-key': $this.data('key'), 'value': '1', 'type': 'checkbox'}));
                $this.find('.mobileconf_list_entry_icon').after($ConfirmationEntryCheckbox);
                $ConfirmationEntryCheckbox.on('click', function(e) {
                    e.stopPropagation();

                    var nChecked = $J('.mobileconf_list_checkbox input:checked').length;
                    var $elButtons = $J('#mobileconf_buttons');

                    if (nChecked > 0) {
                        var $btnCancel = $J('#mobileconf_buttons .mobileconf_button_cancel');
                        var $btnAccept = $J('#mobileconf_buttons .mobileconf_button_accept');
                        $btnCancel.unbind();
                        $btnAccept.unbind();

                        $btnCancel.text('取消选择');
                        $btnAccept.text('确认已选择');

                        $btnCancel.click(function() {
                            ActionForAllSelected('cancel');
                        });

                        $btnAccept.click(function() {
                            ActionForAllSelected('allow');
                        });

                        if ($elButtons.is(':hidden')) {
                            $elButtons.css('bottom', -$elButtons.height() + 'px');
                            $elButtons.show();
                        }
                        $elButtons.css('bottom', '0');
                    } else {
                        $elButtons.css('bottom', -$elButtons.height() + 'px');
                    }
                });
            }
        });

        var $ResponsiveHeaderContent = $J('.responsive_header_content');
        var $ConfirmationCheckAll = $J('<div/>', {'class': 'btn_green_steamui btn_medium'}).append('<span>全选</span>');
        var $ConfirmationRefresh = $J('<div/>', {'class': 'btn_blue_steamui btn_medium'}).append('<span>刷新</span>');
        $ResponsiveHeaderContent.append($J('<div/>', {'style': 'position: absolute; top: 15px; right: 8px;'}).append($ConfirmationCheckAll, '\n', $ConfirmationRefresh));
        $ConfirmationCheckAll.on('click', function() {
            if ($J('#mobileconf_list').is(':visible') && $J('#mobileconf_details').is(':hidden')) {
                $J('.mobileconf_list_checkbox input:not(:checked)').click();
            }
        });
        $ConfirmationRefresh.on('click', function() {
            if (account) {
                window.location.replace('https://steamcommunity.com/mobileconf/conf?' + generateConfirmationQueryParams(account, 'conf', timeOffset));
            } else {
                window.location.reload();
            }
        });
    }

    var intersectionObserver = new IntersectionObserver(function(entries) {
        if (entries[0].intersectionRatio > 0) {
            var name = $J('#login_twofactorauth_message_entercode_accountname, [class^="login_SigningInAccountName"], [class^="newlogindialog_AccountName"]').text();
            $J.each(accounts, function(i, v) {
                if(name == v.name) {
                    var $AuthCodeInput = $J('#twofactorcode_entry, [class^="login_AuthenticatorInputcontainer"] input.DialogInput, [class^="newlogindialog_SegmentedCharacterInput"] input, [class^="segmentedinputs_SegmentedCharacterInput"] input');
                    var dt = new DataTransfer();
                    dt.setData('text', generateAuthCode(v.secret, timeOffset));
                    $AuthCodeInput[0].dispatchEvent(new ClipboardEvent('paste', {clipboardData: dt, bubbles: true}));
                    return false;
                }
            });
        }
    });

    var mutationObserver = new MutationObserver(function() {
        if ($J('#twofactorcode_entry, [class^="login_AuthenticatorInputcontainer"] input.DialogInput, [class^="newlogindialog_SegmentedCharacterInput"] input, [class^="segmentedinputs_SegmentedCharacterInput"] input').length) {
            intersectionObserver.observe($J('#twofactorcode_entry, [class^="login_AuthenticatorInputcontainer"] input.DialogInput, [class^="newlogindialog_SegmentedCharacterInput"] input, [class^="segmentedinputs_SegmentedCharacterInput"] input')[0]);
        }

        if ($J('[class^="newlogindialog_EnterCodeInsteadLink"] [class^="newlogindialog_TextLink"]').length) {
            $J('[class^="newlogindialog_EnterCodeInsteadLink"] [class^="newlogindialog_TextLink"]')[0].click();
        }
    });

    var userSteamID;

    if ($J('#account_dropdown .persona').length) {
        if (typeof g_steamID != 'undefined' && g_steamID) {
            userSteamID = g_steamID;
            createConfirmationLink(userSteamID);
        } else {
            GM_xmlhttpRequest({
                method: 'GET',
                url: 'https://steamcommunity.com/my/?xml=1',
                onload: function(response) {
                    if (response.responseXML) {
                        var steamID = $J(response.responseXML).find('steamID64').text();
                        if (steamID) {
                            userSteamID = steamID;
                            createConfirmationLink(userSteamID);
                        }
                    }
                }
            });
        }
        if (window.location.href.indexOf('checkout.steampowered.com/login/?purchasetype=') !== -1) {
            mutationObserver.observe(document.body, {childList: true, subtree: true});
        }
    } else {
        mutationObserver.observe(document.body, {childList: true, subtree: true});
    }

    var timeOffset = 0;

    GM_xmlhttpRequest({
        method: 'POST',
        url: 'https://api.steampowered.com/ITwoFactorService/QueryTime/v0001',
        responseType: 'json',
        onload: function(response) {
            if (response.response && response.response.response && response.response.response.server_time) {
                timeOffset = response.response.response.server_time - Math.floor(Date.now() / 1000);
            }
        }
    });
})();