// ==UserScript== // @name Humble Choice Get Key // @namespace http://tampermonkey.net/ // @version 0.09 // @description get game key or select game // @author ku mi // @match https://www.humblebundle.com/subscription/* // @grant GM_addStyle // @require https://cdn.staticfile.org/html2canvas/0.5.0-beta4/html2canvas.min.js // @downloadURL none // ==/UserScript== (async function () { const countryMap = { AD: '安道尔', AE: '阿拉伯联合酋长国', AF: '阿富汗', AG: '安提瓜和巴布达', AI: '安圭拉', AL: '阿尔巴尼亚', AM: '亚美尼亚', AO: '安哥拉', AQ: '南极洲', AR: '阿根廷', AS: '美属萨摩亚', AT: '奥地利', AU: '澳大利亚', AW: '阿鲁巴', AX: '奥兰群岛', AZ: '阿塞拜疆', BA: '波斯尼亚和黑塞哥维那', BB: '巴巴多斯', BD: '孟加拉', BE: '比利时', BF: '布基纳法索', BG: '保加利亚', BH: '巴林', BI: '布隆迪', BJ: '贝宁', BL: '圣巴托洛缪岛', BM: '百慕大', BN: '文莱', BO: '玻利维亚', BQ: '博奈尔', BR: '巴西', BS: '巴哈马', BT: '不丹', BU: '缅甸', BV: '布韦岛', BW: '博兹瓦纳', BY: '白俄罗斯', BZ: '伯利兹', CA: '加拿大', CC: '科科斯(基林)群岛', CD: '刚果(金)', CF: '中非共和国', CG: '刚果(布)', CH: '瑞士', CI: '科特迪瓦', CK: '库克群岛', CL: '智利', CM: '喀麦隆', CN: '中国', CO: '哥伦比亚', CR: '哥斯达黎加', CS: '塞尔维亚和黑山', CU: '古巴', CV: '佛得角', CW: '库拉索', CX: '圣诞岛', CY: '塞浦路斯', CZ: '捷克', DE: '德国', DJ: '吉布提', DK: '丹麦', DM: '多米尼克', DO: '多米尼加', DZ: '阿尔及利亚', EC: '厄瓜多尔', EE: '爱沙尼亚', EG: '埃及', EH: '西撒哈拉', ER: '厄立特里亚', ES: '西班牙', ET: '埃塞俄比亚', FI: '芬兰', FJ: '斐济', FK: '福克兰群岛', FM: '密克罗尼西亚', FO: '法罗群岛', FR: '法国', GA: '加蓬', GB: '英国', GD: '格林纳达', GE: '格鲁吉亚', GF: '法属圭亚那', GG: '根西', GH: '加纳', GI: '直布罗陀', GL: '格陵兰', GM: '冈比亚', GN: '几内亚', GP: '瓜德鲁普', GQ: '赤道几内亚', GR: '希腊', GS: '南乔治亚岛和南桑威奇群岛', GT: '危地马拉', GU: '关岛', GW: '几内亚比绍', GY: '圭亚那', HK: '香港', HM: '赫德岛和麦克唐纳群岛', HN: '洪都拉斯', HR: '克罗地亚', HT: '海地', HU: '匈牙利', ID: '印尼', IE: '爱尔兰', IL: '以色列', IM: '马恩岛', IN: '印度', IO: '英属印度洋领地', IQ: '伊拉克', IR: '伊朗', IS: '冰岛', IT: '意大利', JE: '泽西岛', JM: '牙买加', JO: '约旦', JP: '日本', KE: '肯尼亚', KG: '吉尔吉斯', KH: '柬埔寨', KI: '基里巴斯', KM: '科摩罗', KN: '圣基茨和尼维斯', KP: '朝鲜', KR: '韩国', KW: '科威特', KY: '开曼群岛', KZ: '哈萨克斯坦', LA: '老挝', LB: '黎巴嫩', LC: '圣卢西亚', LI: '列支敦士登', LK: '斯里兰卡', LR: '利比里亚', LS: '莱索托', LT: '立陶宛', LU: '卢森堡', LV: '拉脱维亚', LY: '利比亚', MA: '摩洛哥', MC: '摩纳哥', MD: '摩尔多瓦', ME: '黑山', MF: '法属圣马丁', MG: '马达加斯加', MH: '马绍尔群岛', MK: '马其顿', ML: '马里', MM: '缅甸', MN: '蒙古', MO: '澳门', MP: '北马里亚纳群岛', MQ: '马提尼克', MR: '毛里塔尼亚', MS: '蒙塞拉特', MT: '马耳他', MU: '毛里求斯', MV: '马尔代夫', MW: '马拉维', MX: '墨西哥', MY: '马来西亚', MZ: '莫桑比克', NA: '纳米比亚', NC: '新喀里多尼亚', NE: '尼日尔', NF: '诺福克岛', NG: '尼日利', NI: '尼加拉瓜', NL: '荷兰', NO: '挪威', NP: '尼泊尔', NR: '瑙鲁', NU: '纽埃', NZ: '新西兰', OM: '阿曼', PA: '巴拿马', PE: '秘鲁', PF: '法属波利尼西亚a', PG: '巴布亚新几内亚', PH: '菲律宾', PK: '巴基斯坦', PL: '波兰', PM: '圣皮埃尔和密克隆', PN: '皮特凯恩群岛', PR: '波多黎各', PS: '巴勒斯坦', PT: '葡萄牙', PW: '帕劳', PY: '巴拉圭', QA: '卡塔尔', RE: '留尼旺島', RO: '罗马尼亚', RS: '塞尔维亚', RU: '俄罗斯', RW: '卢旺达', SA: '沙特阿拉伯', SB: '所罗门群岛', SC: '塞舌尔', SD: '苏丹', SE: '瑞典', SG: '新加坡', SH: '圣赫勒拿、阿森松与特斯坦达库尼亚', SI: '斯洛文尼', SJ: '斯瓦尔巴群岛和扬马延岛', SK: '斯洛伐克', SL: '塞拉利昂', SM: '圣马力诺', SN: '塞内加尔', SO: '索马里', SR: '苏里南', SS: '南苏丹', ST: '圣多美和普林西比', SV: '萨尔瓦多', SX: '荷属圣马丁', SY: '叙利亚', SZ: '斯威士兰', TC: '特克斯和凯科斯群岛', TD: '乍得', TF: '法属南部领土', TG: '多哥', TH: '泰国', TJ: '塔吉克斯坦', TK: '托克劳', TL: '东帝汶', TM: '土库曼斯坦', TN: '突尼斯', TO: '汤加', TR: '土耳其', TT: '特立尼达和多巴哥', TV: '图瓦卢', TW: '台湾', TZ: '坦桑尼亚', UA: '乌克兰', UG: '乌干达', UM: '美国本土外小岛屿', US: '美国', UY: '乌拉圭', UZ: '乌兹别克斯坦', VA: '圣座', VC: '圣文森特和格林纳丁斯', VE: '委内瑞拉', VG: '英属维尔京群岛', VI: '美属维尔京群岛', VN: '越南', VU: '瓦努阿图', WF: '瓦利斯和富图纳群岛', WS: '萨摩亚', XK: '科索沃', YE: '也门', YT: '马约特', ZA: '南非', ZM: '赞比亚', ZW: '津巴布韦', } async function http(data, flag) { const res = await fetch(data.url, { method: data.method || 'GET', body: data.body || null, headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'} }) if(res.status !== 200) return {} return await flag ? res.text() : res.json() } const res = await http({url: location.href}, true) getInitData(new DOMParser().parseFromString(res, 'text/html')) function getInitData (el) { const script = el.querySelector('#webpack-monthly-product-data') || el.querySelector('#webpack-subscriber-hub-data') if(!script) return const {contentChoiceData, gamekey, contentChoicesMade, downloadPageUrl} = JSON.parse(script.innerText.trim()).contentChoiceOptions if(!gamekey) return const {content_choices, display_order, total_choices} = contentChoiceData.initial const selecedGame = contentChoicesMade ? contentChoicesMade.initial.choices_made : [] const allGame = display_order.map(item => { let i = content_choices[item] return { machine_name: i.tpkds[0].machine_name, title: i.title, exclusive: i.tpkds[0].exclusive_countries, disallowed: i.tpkds[0].disallowed_countries, appid: i.tpkds[0].steam_app_id || '', name: item, key: i.tpkds[0].redeemed_key_val || '' } }) const gameBox = document.createElement('div') const optionBox = document.createElement('div') gameBox.innerHTML = `
注: 锁区信息仅供参考,以激活后的SUB为准!Download页面
` optionBox.innerHTML = `
` const gamelist = gameBox.querySelector('._self_view_') const sButton = gameBox.querySelector('._sh_hd_') const sMask = gameBox.querySelector('._mask_') const sAlert = sMask.firstElementChild const textArr = ['选择游戏(只选不刮)','刮开/提取', '全选高亮', '取消高亮', '多选高亮截图'] const classArr = ['._copy_','._clear_','._option_ul_','._select_ul_', '._key_value_'] const [cButton, clearKey, optionUl, selectUl, keyValue] = classArr.map(item => optionBox.querySelector(item)) const [selectKey, getKey, allLight, noLight, screenShot] = textArr.map((item, index) => { const li = document.createElement('li') li.innerText = item optionUl.appendChild(li) return li }) const liChild = allGame.map((item, index) => { const li = document.createElement('li') li.innerText = index + 1 Object.assign(li.dataset, { name: item.name, title: item.title, key: item.key, }) selectUl.appendChild(li) return li }) setNum() optionUl.onselectstart = () => false selectUl.onselectstart = () => false selectUl.addEventListener('click',(e) => {if (e.target.nodeName === 'LI') e.target.classList.toggle('current')}) allLight.addEventListener('click', () => liChild.forEach(item => item.classList.add('current'))) noLight.addEventListener('click', () => liChild.forEach(item => item.classList.remove('current'))) clearKey.addEventListener('click',() => (keyValue.value = '')) cButton.addEventListener('click', () => { if(!keyValue.value.length) return keyValue.select() document.execCommand('copy') }) clearKey.addEventListener('click', () => (keyValue.value = '')) sButton.addEventListener('click', function () { const flag = this.classList.contains('current') gamelist.classList.remove(flag ? '_slide_down_' : '_slide_up_') gamelist.classList.add(flag ? '_slide_up_': '_slide_down_') this.innerText = flag ? '显示锁区信息' : '隐藏锁区信息' this.classList.toggle('current') }) getKey.addEventListener('click', () => selectGet()) selectKey.addEventListener('click', () => selectGet(true)) screenShot.addEventListener('click', () => { let flag if(!sButton.classList.contains('current')) sButton.click() const arr = [...gamelist.children].filter((item, index) => !liChild[index].classList.contains('current')) if(arr.length !== liChild.length) arr.forEach(item => (item.style.display = 'none')) alertFun(`正在截图点击可取消...(,,•́ . •̀,,)`, true) sMask.onclick = () => { arr.forEach(item => (item.style.display = 'block')) sMask.style.display = 'none' flag = true sMask.onclick = null } html2canvas(gamelist).then(canvas => { if(flag) return sAlert.style.backgroundColor = '#494f5c' alertFun(``,true) sMask.onclick = () => { arr.forEach(item => (item.style.display = 'block')) sMask.style.display = 'none' sAlert.style.backgroundColor = '#c93756' sMask.onclick = null noLight.click() } }) }) gameBox.insertBefore(optionBox, sMask) allGame.forEach(item => { const li = document.createElement('li') li.innerHTML = `
${item.title}

${getLock(item)}

` gamelist.appendChild(li) }) const selectList = gamelist.querySelectorAll('li button:nth-child(2n+1)') const getList = gamelist.querySelectorAll('li button:nth-child(2n)') const view = document.querySelector('.content-choices-view') const list = document.querySelector(".content-choice-tiles.js-content-choice-tiles") view.insertBefore(gameBox, list) selectList.forEach(item => item.addEventListener('click', function(e){ if(this.classList.contains('current')) return sendRquest(true, [this], e) })) getList.forEach(item => item.addEventListener('click', async function(e){ if(this.classList.contains('current')) return if(!selecedGame.includes(this.dataset.name)){ const res = await sendRquest(false, [this], e) if(!res) return alertFun(`刮开失败!๐·°(৹˃̵﹏˂̵৹)°·๐`) } sendKeyRquest([this], e) })) function getZhName(arr) { return arr.map(item => { if (/(\u53f0\u6e7e|\u4e2d\u56fd|\u9999\u6e2f|\u6fb3\u95e8)/.test(countryMap[item])) return `${countryMap[item]}` return countryMap[item] }).join('、') } function getLock(game) { if (game.exclusive.length) return `只能在以下激活: ${getZhName(game.exclusive)}` if (game.disallowed.length) return `不能在以下地区激活: ${getZhName(game.disallowed)}` return `无限制激活` } function alertFun(msg, flag) { sAlert.innerHTML = msg sMask.style.display = 'block' if(flag) return let time = setTimeout(() => { sMask.style.display = 'none' clearTimeout(time) }, 1200) } async function selectGet(flag) { if (flag && selecedGame.length >= total_choices) return alertFun(`已经没得选择了!(。•ˇ‸ˇ•。)`) const noSelect = [] const noSelectLi = [] const selected = [] const no = flag ? selectList : getList const arr = liChild.filter((li,index) => { if(li.classList.contains('current') && (selected.length + noSelect.length < total_choices)) { if(!flag && selecedGame.includes(li.dataset.name) && !li.dataset.key) selected.push(no[index]) else if(!selecedGame.includes(li.dataset.name) && (noSelect.length + selecedGame.length < total_choices)){ noSelect.push(no[index]) noSelectLi.push(li) } } return li.classList.contains('current') }) if(!arr.length) return if(arr.length > total_choices) return alertFun(`最多选${total_choices}个!ヽ(#\`Д´)ノ`) if(flag && !noSelect.length) return alertFun(`已经选过了!ヽ(#\`Д´)ノ`) noLight.click() if(noSelect.length){ const res = await sendRquest(flag, noSelect) if(!res && !flag) return alertFun(`刮开失败!๐·°(৹˃̵﹏˂̵৹)°·๐`) if(flag) return arr.forEach(item => selecedGame.includes(item.dataset.name) && (keyValue.value += `${item.dataset.title}: ${noSelectLi.includes(item) ? '选择成功' : '重复选择'}\n`)) } if(selected.length || noSelect.length) await sendKeyRquest(selected.concat(noSelect)) keyValue.value = '' arr.forEach(item => item.dataset.key && (keyValue.value += `${item.dataset.title}: ${item.dataset.key}\n`)) } async function sendKeyRquest (arr, e){ alertFun(`正在努力请求...!(,,•́ . •̀,,)`,true) keyValue.value = '' const res = await Promise.all(arr.map(item => { return http({ url: 'https://www.humblebundle.com/humbler/redeemkey', body: `keytype=${item.dataset.machine_name}&key=${gamekey}&keyindex=0`, method: 'POST' }) })) alertFun(`已经刮好了!(๑˃́ꇴ˂̀๑)`) arr.forEach((item, index) => { liChild[display_order.findIndex(name => name === item.dataset.name)].dataset.key = res[index].key || '刷新重试' item.innerText = '已刮开' item.className = 'current' const input = item.nextElementSibling.nextElementSibling input.style.display = 'block' input.value = res[index].key || '刷新重试' if(e) keyValue.value += `${item.dataset.title}: ${res[index].key}\n` }) } async function sendRquest(flag, arr, e) { keyValue.value = '' let url = `https://www.humblebundle.com/humbler/choosecontent?gamekey=${gamekey}&parent_identifier=initial` arr.forEach(item => { url += `&chosen_identifiers%5B%5D=${item.dataset.name}` }) alertFun(`正在努力请求...!(,,•́ . •̀,,)`,true) const res = await http({url}) if(flag) alertFun(res.force_refresh ? `已经选择好了!(๑˃́ꇴ˂̀๑)` : `选择失败!๐·°(৹˃̵﹏˂̵৹)°·๐`) arr.forEach(item => { if(res.force_refresh) { if(flag) { item.innerText = '已选择' item.className = 'current' }else { const next = item.nextElementSibling next.innerText = '已选择' next.className = 'current' } selecedGame.push(item.dataset.name) } if(e && flag) keyValue.value += `${item.dataset.title}: ${res.force_refresh ? '选择成功' : '选择失败'}\n` }) return res.force_refresh } function setNum() { const els = document.querySelectorAll('.js-content-choices .choice-image-container') els.forEach((item, index) => { let div = document.createElement('div') div.setAttribute('class','_game_num_') div.innerText = index + 1 item.appendChild(div) }) } GM_addStyle(`._click_key_{border-radius:4px;outline:none;border:none;background-color:#454c5e;padding:5px 15px;float:right;}._mask_{position:fixed;left:0;top:0;bottom:0;right:0;display:none;background-color:rgba(0,0,0,.3);z-index:999;}._alert_{background-color:#c93756;border-radius:10px;box-shadow:0 0 15px #c93756;font-size:1.5em;position:absolute;text-align:center;left:50%;top:50%;transform:translate(-50%,-50%);padding:0 30px;line-height:3em;}._sh_hd_{border:none;outline:none;color:#A4D7F5;background-image:linear-gradient( to bottom,rgba(47,137,188,1) 5%,rgba(23,67,92,1) 95%);margin:20px 0 0 20px;border-radius:5px;line-height:50px;padding:0 20px;}._slide_down_{animation:slideDown .3s forwards;}._slide_up_{animation:slideUp .3s forwards;}@keyframes slideUp{0%{max-height:2000px;}100%{max-height:0;}}@keyframes slideDown{0%{max-height:0;}100%{max-height:2000px;}}._down_page_{float:right;padding:0 20px;border-radius:5px;height:50px;margin:20px 20px 0 0;line-height:50px;background:linear-gradient( to bottom,rgba(47,137,188,1) 5%,rgba(23,67,92,1) 95%);color:#A4D7F5;text-decoration:none;}._key_value_{margin:20px 0 0 20px;width:650px;height:200px;resize:none;font-size:18px;color:#fff;outline:none;background-color:#454c5e;border:none;}._option_ul_,._select_ul_{margin:30px 0 0 20px;height:50px;line-height:50px;display:flex;list-style:none;padding:0;}._option_ul_>li,._select_ul_>li{height:50px;line-height:50px;color:#A4D7F5;background-image:linear-gradient( to bottom,rgba(47,137,188,1) 5%,rgba(23,67,92,1) 95%);margin-right:20px;border-radius:5px;text-align:center;padding:0 20px;font-size:16px;cursor:pointer;}._select_ul_>li{background:#454c5e;}._select_ul_>li.current{background-image:linear-gradient(to top,#0c3483 0%,#a2b6df 100%,#6b8cce 100%,#a2b6df 100%);}._game_num_{width:100%;height:100%;position:absolute;left:0;top:0;z-index:1;background-color:rgba(0,0,0,.3);text-align:center;font-size:100px;}._sh_hd_.current{background-image:linear-gradient(to top,#c71d6f 0%,#d09693 100%);color:#fff;}._self_view_{max-height:2000px;list-style:none;margin:20px 0 0 0;padding:0;overflow:hidden;}._self_view_>li{font-size:16px;padding:20px 0 0px 20px;border-bottom:10px solid #454c5e;display:flex;justify-content:space-between;}._self_view_>li button{border:none;outline:none;background-image:linear-gradient( to bottom,rgba(47,137,188,1) 5%,rgba(23,67,92,1) 95%);color:#A4D7F5;margin:0 10px;font-size:16px;border-radius:0.8em;padding:5px 15px;width:100px;float:right;}._copy_,._clear_{background:linear-gradient( to bottom,rgba(47,137,188,1) 5%,rgba(23,67,92,1) 95%);color:#A4D7F5;border-radius:5px;border:none;outline:none;font-size:16px;line-height:50px;text-align:center;margin:170px 0 0 20px;width:70px;}._self_view_>li .current{opacity:0.45;}._self_view_>li:last-child{border:none;}`)} })();